整合資料庫、完成登入註冊忘記密碼功能
This commit is contained in:
201
lib/database.ts
201
lib/database.ts
@@ -1,3 +1,7 @@
|
||||
// =====================================================
|
||||
// 資料庫連接配置
|
||||
// =====================================================
|
||||
|
||||
import mysql from 'mysql2/promise';
|
||||
|
||||
// 資料庫配置
|
||||
@@ -9,205 +13,114 @@ const dbConfig = {
|
||||
database: process.env.DB_NAME || 'db_AI_Platform',
|
||||
charset: 'utf8mb4',
|
||||
timezone: '+08:00',
|
||||
// 連接池配置
|
||||
connectionLimit: 10,
|
||||
acquireTimeout: 60000,
|
||||
timeout: 60000,
|
||||
reconnect: true
|
||||
reconnect: true,
|
||||
connectionLimit: 10,
|
||||
queueLimit: 0,
|
||||
};
|
||||
|
||||
// 創建連接池
|
||||
let pool: mysql.Pool | null = null;
|
||||
const pool = mysql.createPool(dbConfig);
|
||||
|
||||
// 資料庫連接類
|
||||
export class Database {
|
||||
private static instance: Database;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
private pool: mysql.Pool;
|
||||
|
||||
private constructor() {
|
||||
this.pool = pool;
|
||||
}
|
||||
|
||||
public static getInstance(): Database {
|
||||
if (!Database.instance) {
|
||||
Database.instance = new Database();
|
||||
}
|
||||
return Database.instance;
|
||||
}
|
||||
|
||||
// 獲取連接池
|
||||
public async getPool(): Promise<mysql.Pool> {
|
||||
if (!pool) {
|
||||
pool = mysql.createPool(dbConfig);
|
||||
|
||||
// 測試連接
|
||||
try {
|
||||
const connection = await pool.getConnection();
|
||||
console.log('✅ 資料庫連接池建立成功');
|
||||
connection.release();
|
||||
} catch (error) {
|
||||
console.error('❌ 資料庫連接池建立失敗:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
return pool;
|
||||
|
||||
// 獲取連接
|
||||
public async getConnection(): Promise<mysql.PoolConnection> {
|
||||
return await this.pool.getConnection();
|
||||
}
|
||||
|
||||
|
||||
// 執行查詢
|
||||
public async query<T = any>(sql: string, params?: any[]): Promise<T[]> {
|
||||
const pool = await this.getPool();
|
||||
const connection = await this.getConnection();
|
||||
try {
|
||||
const [rows] = await pool.execute(sql, params);
|
||||
const [rows] = await connection.execute(sql, params);
|
||||
return rows as T[];
|
||||
} catch (error) {
|
||||
console.error('查詢執行失敗:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
connection.release();
|
||||
}
|
||||
}
|
||||
|
||||
// 執行單一查詢 (返回第一筆結果)
|
||||
|
||||
// 執行單一查詢
|
||||
public async queryOne<T = any>(sql: string, params?: any[]): Promise<T | null> {
|
||||
const results = await this.query<T>(sql, params);
|
||||
return results.length > 0 ? results[0] : null;
|
||||
}
|
||||
|
||||
|
||||
// 執行插入
|
||||
public async insert(table: string, data: Record<string, any>): Promise<number> {
|
||||
const columns = Object.keys(data);
|
||||
const values = Object.values(data);
|
||||
const placeholders = columns.map(() => '?').join(', ');
|
||||
|
||||
const sql = `INSERT INTO ${table} (${columns.join(', ')}) VALUES (${placeholders})`;
|
||||
|
||||
const pool = await this.getPool();
|
||||
public async insert(sql: string, params?: any[]): Promise<mysql.ResultSetHeader> {
|
||||
const connection = await this.getConnection();
|
||||
try {
|
||||
const [result] = await pool.execute(sql, values);
|
||||
return (result as any).insertId;
|
||||
} catch (error) {
|
||||
console.error('插入執行失敗:', error);
|
||||
throw error;
|
||||
const [result] = await connection.execute(sql, params);
|
||||
return result as mysql.ResultSetHeader;
|
||||
} finally {
|
||||
connection.release();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 執行更新
|
||||
public async update(table: string, data: Record<string, any>, where: Record<string, any>): Promise<number> {
|
||||
const setColumns = Object.keys(data).map(col => `${col} = ?`).join(', ');
|
||||
const whereColumns = Object.keys(where).map(col => `${col} = ?`).join(' AND ');
|
||||
|
||||
const sql = `UPDATE ${table} SET ${setColumns} WHERE ${whereColumns}`;
|
||||
const values = [...Object.values(data), ...Object.values(where)];
|
||||
|
||||
const pool = await this.getPool();
|
||||
public async update(sql: string, params?: any[]): Promise<mysql.ResultSetHeader> {
|
||||
const connection = await this.getConnection();
|
||||
try {
|
||||
const [result] = await pool.execute(sql, values);
|
||||
return (result as any).affectedRows;
|
||||
} catch (error) {
|
||||
console.error('更新執行失敗:', error);
|
||||
throw error;
|
||||
const [result] = await connection.execute(sql, params);
|
||||
return result as mysql.ResultSetHeader;
|
||||
} finally {
|
||||
connection.release();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 執行刪除
|
||||
public async delete(table: string, where: Record<string, any>): Promise<number> {
|
||||
const whereColumns = Object.keys(where).map(col => `${col} = ?`).join(' AND ');
|
||||
const sql = `DELETE FROM ${table} WHERE ${whereColumns}`;
|
||||
const values = Object.values(where);
|
||||
|
||||
const pool = await this.getPool();
|
||||
public async delete(sql: string, params?: any[]): Promise<mysql.ResultSetHeader> {
|
||||
const connection = await this.getConnection();
|
||||
try {
|
||||
const [result] = await pool.execute(sql, values);
|
||||
return (result as any).affectedRows;
|
||||
} catch (error) {
|
||||
console.error('刪除執行失敗:', error);
|
||||
throw error;
|
||||
const [result] = await connection.execute(sql, params);
|
||||
return result as mysql.ResultSetHeader;
|
||||
} finally {
|
||||
connection.release();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 開始事務
|
||||
public async beginTransaction(): Promise<mysql.PoolConnection> {
|
||||
const pool = await this.getPool();
|
||||
const connection = await pool.getConnection();
|
||||
const connection = await this.getConnection();
|
||||
await connection.beginTransaction();
|
||||
return connection;
|
||||
}
|
||||
|
||||
|
||||
// 提交事務
|
||||
public async commitTransaction(connection: mysql.PoolConnection): Promise<void> {
|
||||
public async commit(connection: mysql.PoolConnection): Promise<void> {
|
||||
await connection.commit();
|
||||
connection.release();
|
||||
}
|
||||
|
||||
|
||||
// 回滾事務
|
||||
public async rollbackTransaction(connection: mysql.PoolConnection): Promise<void> {
|
||||
public async rollback(connection: mysql.PoolConnection): Promise<void> {
|
||||
await connection.rollback();
|
||||
connection.release();
|
||||
}
|
||||
|
||||
|
||||
// 關閉連接池
|
||||
public async close(): Promise<void> {
|
||||
if (pool) {
|
||||
await pool.end();
|
||||
pool = null;
|
||||
console.log('🔌 資料庫連接池已關閉');
|
||||
}
|
||||
}
|
||||
|
||||
// 健康檢查
|
||||
public async healthCheck(): Promise<boolean> {
|
||||
try {
|
||||
const result = await this.queryOne('SELECT 1 as health');
|
||||
return result?.health === 1;
|
||||
} catch (error) {
|
||||
console.error('資料庫健康檢查失敗:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 獲取資料庫統計
|
||||
public async getDatabaseStats(): Promise<{
|
||||
tables: number;
|
||||
users: number;
|
||||
competitions: number;
|
||||
apps: number;
|
||||
judges: number;
|
||||
}> {
|
||||
try {
|
||||
const tablesResult = await this.queryOne(`
|
||||
SELECT COUNT(*) as count
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = ?
|
||||
`, [dbConfig.database]);
|
||||
|
||||
const usersResult = await this.queryOne('SELECT COUNT(*) as count FROM users');
|
||||
const competitionsResult = await this.queryOne('SELECT COUNT(*) as count FROM competitions');
|
||||
const appsResult = await this.queryOne('SELECT COUNT(*) as count FROM apps');
|
||||
const judgesResult = await this.queryOne('SELECT COUNT(*) as count FROM judges');
|
||||
|
||||
return {
|
||||
tables: tablesResult?.count || 0,
|
||||
users: usersResult?.count || 0,
|
||||
competitions: competitionsResult?.count || 0,
|
||||
apps: appsResult?.count || 0,
|
||||
judges: judgesResult?.count || 0
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('獲取資料庫統計失敗:', error);
|
||||
throw error;
|
||||
}
|
||||
await this.pool.end();
|
||||
}
|
||||
}
|
||||
|
||||
// 導出單例實例
|
||||
export const db = Database.getInstance();
|
||||
|
||||
import bcrypt from 'bcrypt';
|
||||
|
||||
// 工具函數
|
||||
export const generateId = (): string => {
|
||||
return Date.now().toString(36) + Math.random().toString(36).substr(2);
|
||||
};
|
||||
|
||||
export const hashPassword = async (password: string): Promise<string> => {
|
||||
const saltRounds = 12;
|
||||
return bcrypt.hash(password, saltRounds);
|
||||
};
|
||||
|
||||
export const comparePassword = async (password: string, hash: string): Promise<boolean> => {
|
||||
return bcrypt.compare(password, hash);
|
||||
};
|
||||
// 導出類型
|
||||
export type { PoolConnection } from 'mysql2/promise';
|
||||
|
353
lib/models.ts
Normal file
353
lib/models.ts
Normal file
@@ -0,0 +1,353 @@
|
||||
// =====================================================
|
||||
// 資料庫模型定義
|
||||
// =====================================================
|
||||
|
||||
// 用戶模型
|
||||
export interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
password_hash: string;
|
||||
avatar?: string;
|
||||
department: string;
|
||||
role: 'user' | 'developer' | 'admin';
|
||||
join_date: string;
|
||||
total_likes: number;
|
||||
total_views: number;
|
||||
is_active: boolean;
|
||||
last_login?: string;
|
||||
phone?: string;
|
||||
location?: string;
|
||||
bio?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
// 前端使用的 User 類型(不包含密碼)
|
||||
export interface UserProfile {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
avatar?: string;
|
||||
department: string;
|
||||
role: 'user' | 'developer' | 'admin';
|
||||
join_date: string;
|
||||
total_likes: number;
|
||||
total_views: number;
|
||||
is_active: boolean;
|
||||
last_login?: string;
|
||||
phone?: string;
|
||||
location?: string;
|
||||
bio?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
// 評審模型
|
||||
export interface Judge {
|
||||
id: string;
|
||||
name: string;
|
||||
title: string;
|
||||
department: string;
|
||||
expertise: string[];
|
||||
avatar?: string;
|
||||
is_active: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
// 團隊模型
|
||||
export interface Team {
|
||||
id: string;
|
||||
name: string;
|
||||
leader_id: string;
|
||||
department: string;
|
||||
contact_email: string;
|
||||
total_likes: number;
|
||||
is_active: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
// 團隊成員模型
|
||||
export interface TeamMember {
|
||||
id: string;
|
||||
team_id: string;
|
||||
user_id: string;
|
||||
role: string;
|
||||
joined_at: string;
|
||||
}
|
||||
|
||||
// 競賽模型
|
||||
export interface Competition {
|
||||
id: string;
|
||||
name: string;
|
||||
year: number;
|
||||
month: number;
|
||||
start_date: string;
|
||||
end_date: string;
|
||||
status: 'upcoming' | 'active' | 'judging' | 'completed';
|
||||
description?: string;
|
||||
type: 'individual' | 'team' | 'mixed' | 'proposal';
|
||||
evaluation_focus?: string;
|
||||
max_team_size?: number;
|
||||
is_active: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
// 競賽規則模型
|
||||
export interface CompetitionRule {
|
||||
id: string;
|
||||
competition_id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
weight: number;
|
||||
order_index: number;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
// 競賽獎項類型模型
|
||||
export interface CompetitionAwardType {
|
||||
id: string;
|
||||
competition_id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
icon: string;
|
||||
color: string;
|
||||
is_active: boolean;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
// 應用模型
|
||||
export interface App {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
creator_id: string;
|
||||
team_id?: string;
|
||||
category: string;
|
||||
type: string;
|
||||
likes_count: number;
|
||||
views_count: number;
|
||||
rating: number;
|
||||
is_active: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
// 提案模型
|
||||
export interface Proposal {
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
problem_statement: string;
|
||||
solution: string;
|
||||
expected_impact: string;
|
||||
team_id: string;
|
||||
attachments?: string[];
|
||||
status: 'draft' | 'submitted' | 'under_review' | 'approved' | 'rejected';
|
||||
submitted_at?: string;
|
||||
is_active: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
// 應用評分模型
|
||||
export interface AppJudgeScore {
|
||||
id: string;
|
||||
judge_id: string;
|
||||
app_id: string;
|
||||
innovation_score: number;
|
||||
technical_score: number;
|
||||
usability_score: number;
|
||||
presentation_score: number;
|
||||
impact_score: number;
|
||||
total_score: number;
|
||||
comments?: string;
|
||||
submitted_at: string;
|
||||
}
|
||||
|
||||
// 提案評分模型
|
||||
export interface ProposalJudgeScore {
|
||||
id: string;
|
||||
judge_id: string;
|
||||
proposal_id: string;
|
||||
problem_identification_score: number;
|
||||
solution_feasibility_score: number;
|
||||
innovation_score: number;
|
||||
impact_score: number;
|
||||
presentation_score: number;
|
||||
total_score: number;
|
||||
comments?: string;
|
||||
submitted_at: string;
|
||||
}
|
||||
|
||||
// 獎項模型
|
||||
export interface Award {
|
||||
id: string;
|
||||
competition_id: string;
|
||||
app_id?: string;
|
||||
team_id?: string;
|
||||
proposal_id?: string;
|
||||
app_name?: string;
|
||||
team_name?: string;
|
||||
proposal_title?: string;
|
||||
creator: string;
|
||||
award_type: 'gold' | 'silver' | 'bronze' | 'popular' | 'innovation' | 'technical' | 'custom';
|
||||
award_name: string;
|
||||
score: number;
|
||||
year: number;
|
||||
month: number;
|
||||
icon: string;
|
||||
custom_award_type_id?: string;
|
||||
competition_type: 'individual' | 'team' | 'proposal';
|
||||
rank: number;
|
||||
category: 'innovation' | 'technical' | 'practical' | 'popular' | 'teamwork' | 'solution' | 'creativity';
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
// 用戶收藏模型
|
||||
export interface UserFavorite {
|
||||
id: string;
|
||||
user_id: string;
|
||||
app_id: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
// 用戶按讚模型
|
||||
export interface UserLike {
|
||||
id: string;
|
||||
user_id: string;
|
||||
app_id: string;
|
||||
liked_at: string;
|
||||
}
|
||||
|
||||
// 用戶瀏覽記錄模型
|
||||
export interface UserView {
|
||||
id: string;
|
||||
user_id: string;
|
||||
app_id: string;
|
||||
viewed_at: string;
|
||||
ip_address?: string;
|
||||
user_agent?: string;
|
||||
}
|
||||
|
||||
// 用戶評分模型
|
||||
export interface UserRating {
|
||||
id: string;
|
||||
user_id: string;
|
||||
app_id: string;
|
||||
rating: number;
|
||||
rated_at: string;
|
||||
}
|
||||
|
||||
// 聊天會話模型
|
||||
export interface ChatSession {
|
||||
id: string;
|
||||
user_id: string;
|
||||
session_name?: string;
|
||||
is_active: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
// 聊天訊息模型
|
||||
export interface ChatMessage {
|
||||
id: string;
|
||||
session_id: string;
|
||||
text: string;
|
||||
sender: 'user' | 'bot';
|
||||
quick_questions?: string[];
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
// AI助手配置模型
|
||||
export interface AIAssistantConfig {
|
||||
id: string;
|
||||
api_key: string;
|
||||
api_url: string;
|
||||
model: string;
|
||||
max_tokens: number;
|
||||
temperature: number;
|
||||
system_prompt: string;
|
||||
is_active: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
// 系統設定模型
|
||||
export interface SystemSetting {
|
||||
id: string;
|
||||
key: string;
|
||||
value: string;
|
||||
description?: string;
|
||||
category: string;
|
||||
is_public: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
// 活動日誌模型
|
||||
export interface ActivityLog {
|
||||
id: string;
|
||||
user_id?: string;
|
||||
action: string;
|
||||
resource_type: string;
|
||||
resource_id?: string;
|
||||
details?: any;
|
||||
ip_address?: string;
|
||||
user_agent?: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
// 統計視圖模型
|
||||
export interface UserStatistics {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
department: string;
|
||||
role: string;
|
||||
join_date: string;
|
||||
total_likes: number;
|
||||
total_views: number;
|
||||
favorite_count: number;
|
||||
liked_apps_count: number;
|
||||
viewed_apps_count: number;
|
||||
average_rating_given: number;
|
||||
teams_joined: number;
|
||||
teams_led: number;
|
||||
}
|
||||
|
||||
export interface AppStatistics {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
category: string;
|
||||
type: string;
|
||||
likes_count: number;
|
||||
views_count: number;
|
||||
rating: number;
|
||||
creator_name: string;
|
||||
creator_department: string;
|
||||
team_name?: string;
|
||||
favorite_users_count: number;
|
||||
liked_users_count: number;
|
||||
viewed_users_count: number;
|
||||
average_judge_score: number;
|
||||
judge_count: number;
|
||||
}
|
||||
|
||||
export interface CompetitionStatistics {
|
||||
id: string;
|
||||
name: string;
|
||||
year: number;
|
||||
month: number;
|
||||
type: string;
|
||||
status: string;
|
||||
judge_count: number;
|
||||
app_count: number;
|
||||
team_count: number;
|
||||
proposal_count: number;
|
||||
award_count: number;
|
||||
}
|
621
lib/services/database-service.ts
Normal file
621
lib/services/database-service.ts
Normal file
@@ -0,0 +1,621 @@
|
||||
// =====================================================
|
||||
// 資料庫服務層
|
||||
// =====================================================
|
||||
|
||||
import { db } from '../database';
|
||||
import type {
|
||||
User,
|
||||
Judge,
|
||||
Team,
|
||||
TeamMember,
|
||||
Competition,
|
||||
CompetitionRule,
|
||||
CompetitionAwardType,
|
||||
App,
|
||||
Proposal,
|
||||
AppJudgeScore,
|
||||
ProposalJudgeScore,
|
||||
Award,
|
||||
UserFavorite,
|
||||
UserLike,
|
||||
UserView,
|
||||
UserRating,
|
||||
ChatSession,
|
||||
ChatMessage,
|
||||
AIAssistantConfig,
|
||||
SystemSetting,
|
||||
ActivityLog,
|
||||
UserStatistics,
|
||||
AppStatistics,
|
||||
CompetitionStatistics
|
||||
} from '../models';
|
||||
|
||||
// =====================================================
|
||||
// 用戶服務
|
||||
// =====================================================
|
||||
export class UserService {
|
||||
// 創建用戶
|
||||
async create(userData: Omit<User, 'id' | 'created_at' | 'updated_at'>): Promise<User> {
|
||||
const sql = `
|
||||
INSERT INTO users (id, name, email, password_hash, avatar, department, role, join_date, total_likes, total_views, is_active, last_login)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`;
|
||||
const params = [
|
||||
userData.id,
|
||||
userData.name,
|
||||
userData.email,
|
||||
userData.password_hash,
|
||||
userData.avatar || null,
|
||||
userData.department,
|
||||
userData.role,
|
||||
userData.join_date,
|
||||
userData.total_likes,
|
||||
userData.total_views,
|
||||
userData.is_active,
|
||||
userData.last_login || null
|
||||
];
|
||||
|
||||
await db.insert(sql, params);
|
||||
return await this.findByEmail(userData.email) as User;
|
||||
}
|
||||
|
||||
// 根據郵箱獲取用戶
|
||||
async findByEmail(email: string): Promise<User | null> {
|
||||
const sql = 'SELECT * FROM users WHERE email = ? AND is_active = TRUE';
|
||||
return await db.queryOne<User>(sql, [email]);
|
||||
}
|
||||
|
||||
// 根據ID獲取用戶
|
||||
async findById(id: string): Promise<User | null> {
|
||||
const sql = 'SELECT * FROM users WHERE id = ? AND is_active = TRUE';
|
||||
return await db.queryOne<User>(sql, [id]);
|
||||
}
|
||||
|
||||
// 更新用戶
|
||||
async update(id: string, updates: Partial<User>): Promise<User | null> {
|
||||
const fields = Object.keys(updates).filter(key => key !== 'id' && key !== 'created_at');
|
||||
const setClause = fields.map(field => `${field} = ?`).join(', ');
|
||||
const values = fields.map(field => (updates as any)[field]);
|
||||
|
||||
const sql = `UPDATE users SET ${setClause}, updated_at = CURRENT_TIMESTAMP WHERE id = ?`;
|
||||
const result = await db.update(sql, [...values, id]);
|
||||
|
||||
if (result.affectedRows > 0) {
|
||||
return await this.findById(id);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 更新最後登入時間
|
||||
async updateLastLogin(id: string): Promise<boolean> {
|
||||
const sql = 'UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?';
|
||||
const result = await db.update(sql, [id]);
|
||||
return result.affectedRows > 0;
|
||||
}
|
||||
|
||||
// 獲取用戶統計
|
||||
async getUserStatistics(id: string): Promise<UserStatistics | null> {
|
||||
const sql = 'SELECT * FROM user_statistics WHERE id = ?';
|
||||
return await db.queryOne<UserStatistics>(sql, [id]);
|
||||
}
|
||||
|
||||
// 獲取所有用戶(管理員用)
|
||||
async findAll(filters: {
|
||||
search?: string;
|
||||
department?: string;
|
||||
role?: string;
|
||||
status?: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
} = {}): Promise<{ users: User[]; total: number }> {
|
||||
const { search, department, role, status, page = 1, limit = 10 } = filters;
|
||||
|
||||
// 構建查詢條件
|
||||
let whereConditions = ['is_active = TRUE'];
|
||||
let params: any[] = [];
|
||||
|
||||
if (search) {
|
||||
whereConditions.push('(name LIKE ? OR email LIKE ?)');
|
||||
params.push(`%${search}%`, `%${search}%`);
|
||||
}
|
||||
|
||||
if (department && department !== 'all') {
|
||||
whereConditions.push('department = ?');
|
||||
params.push(department);
|
||||
}
|
||||
|
||||
if (role && role !== 'all') {
|
||||
whereConditions.push('role = ?');
|
||||
params.push(role);
|
||||
}
|
||||
|
||||
if (status && status !== 'all') {
|
||||
if (status === 'active') {
|
||||
whereConditions.push('last_login IS NOT NULL AND last_login >= DATE_SUB(NOW(), INTERVAL 30 DAY)');
|
||||
} else if (status === 'inactive') {
|
||||
whereConditions.push('last_login IS NULL OR last_login < DATE_SUB(NOW(), INTERVAL 30 DAY)');
|
||||
}
|
||||
}
|
||||
|
||||
const whereClause = whereConditions.length > 0 ? `WHERE ${whereConditions.join(' AND ')}` : '';
|
||||
|
||||
// 獲取總數
|
||||
const countSql = `SELECT COUNT(*) as total FROM users ${whereClause}`;
|
||||
const countResult = await this.query(countSql, params);
|
||||
const total = countResult[0]?.total || 0;
|
||||
|
||||
// 獲取用戶列表
|
||||
const offset = (page - 1) * limit;
|
||||
const usersSql = `
|
||||
SELECT
|
||||
id, name, email, avatar, department, role, join_date,
|
||||
total_likes, total_views, is_active, last_login, created_at, updated_at
|
||||
FROM users
|
||||
${whereClause}
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ${offset}, ${limit}
|
||||
`;
|
||||
const users = await this.query<User>(usersSql, params);
|
||||
|
||||
return { users, total };
|
||||
}
|
||||
|
||||
// 獲取用戶統計數據
|
||||
async getUserStats(): Promise<{
|
||||
totalUsers: number;
|
||||
activeUsers: number;
|
||||
adminCount: number;
|
||||
developerCount: number;
|
||||
inactiveUsers: number;
|
||||
newThisMonth: number;
|
||||
}> {
|
||||
const sql = `
|
||||
SELECT
|
||||
COUNT(*) as total_users,
|
||||
COUNT(CASE WHEN last_login IS NOT NULL AND last_login >= DATE_SUB(NOW(), INTERVAL 30 DAY) THEN 1 END) as active_users,
|
||||
COUNT(CASE WHEN role = 'admin' THEN 1 END) as admin_count,
|
||||
COUNT(CASE WHEN role = 'developer' THEN 1 END) as developer_count,
|
||||
COUNT(CASE WHEN last_login IS NULL OR last_login < DATE_SUB(NOW(), INTERVAL 30 DAY) THEN 1 END) as inactive_users,
|
||||
COUNT(CASE WHEN created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY) THEN 1 END) as new_this_month
|
||||
FROM users
|
||||
WHERE is_active = TRUE
|
||||
`;
|
||||
const result = await this.query(sql);
|
||||
const stats = result[0] || {};
|
||||
|
||||
return {
|
||||
totalUsers: stats.total_users || 0,
|
||||
activeUsers: stats.active_users || 0,
|
||||
adminCount: stats.admin_count || 0,
|
||||
developerCount: stats.developer_count || 0,
|
||||
inactiveUsers: stats.inactive_users || 0,
|
||||
newThisMonth: stats.new_this_month || 0
|
||||
};
|
||||
}
|
||||
|
||||
// 通用查詢方法
|
||||
async query<T = any>(sql: string, params: any[] = []): Promise<T[]> {
|
||||
return await db.query<T>(sql, params);
|
||||
}
|
||||
|
||||
// 通用單一查詢方法
|
||||
async queryOne<T = any>(sql: string, params: any[] = []): Promise<T | null> {
|
||||
return await db.queryOne<T>(sql, params);
|
||||
}
|
||||
|
||||
// 獲取所有用戶
|
||||
async getAllUsers(limit = 50, offset = 0): Promise<User[]> {
|
||||
const sql = 'SELECT * FROM users WHERE is_active = TRUE ORDER BY created_at DESC LIMIT ? OFFSET ?';
|
||||
return await db.query<User>(sql, [limit, offset]);
|
||||
}
|
||||
|
||||
// 靜態方法保持向後兼容
|
||||
static async createUser(userData: Omit<User, 'id' | 'created_at' | 'updated_at'>): Promise<User> {
|
||||
const service = new UserService();
|
||||
return await service.create(userData);
|
||||
}
|
||||
|
||||
static async getUserByEmail(email: string): Promise<User | null> {
|
||||
const service = new UserService();
|
||||
return await service.findByEmail(email);
|
||||
}
|
||||
|
||||
static async getUserById(id: string): Promise<User | null> {
|
||||
const service = new UserService();
|
||||
return await service.findById(id);
|
||||
}
|
||||
|
||||
static async updateUser(id: string, updates: Partial<User>): Promise<boolean> {
|
||||
const service = new UserService();
|
||||
const result = await service.update(id, updates);
|
||||
return result !== null;
|
||||
}
|
||||
|
||||
static async getUserStatistics(id: string): Promise<UserStatistics | null> {
|
||||
const service = new UserService();
|
||||
return await service.getUserStatistics(id);
|
||||
}
|
||||
|
||||
static async getAllUsers(limit = 50, offset = 0): Promise<User[]> {
|
||||
const service = new UserService();
|
||||
return await service.getAllUsers(limit, offset);
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// 評審服務
|
||||
// =====================================================
|
||||
export class JudgeService {
|
||||
// 創建評審
|
||||
static async createJudge(judgeData: Omit<Judge, 'id' | 'created_at' | 'updated_at'>): Promise<Judge> {
|
||||
const sql = `
|
||||
INSERT INTO judges (id, name, title, department, expertise, avatar, is_active)
|
||||
VALUES (UUID(), ?, ?, ?, ?, ?, ?)
|
||||
`;
|
||||
const params = [
|
||||
judgeData.name,
|
||||
judgeData.title,
|
||||
judgeData.department,
|
||||
JSON.stringify(judgeData.expertise),
|
||||
judgeData.avatar || null,
|
||||
judgeData.is_active
|
||||
];
|
||||
|
||||
await db.insert(sql, params);
|
||||
return await this.getJudgeByName(judgeData.name) as Judge;
|
||||
}
|
||||
|
||||
// 根據姓名獲取評審
|
||||
static async getJudgeByName(name: string): Promise<Judge | null> {
|
||||
const sql = 'SELECT * FROM judges WHERE name = ? AND is_active = TRUE';
|
||||
const result = await db.queryOne<Judge>(sql, [name]);
|
||||
if (result) {
|
||||
result.expertise = JSON.parse(result.expertise as any);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// 根據ID獲取評審
|
||||
static async getJudgeById(id: string): Promise<Judge | null> {
|
||||
const sql = 'SELECT * FROM judges WHERE id = ? AND is_active = TRUE';
|
||||
const result = await db.queryOne<Judge>(sql, [id]);
|
||||
if (result) {
|
||||
result.expertise = JSON.parse(result.expertise as any);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// 獲取所有評審
|
||||
static async getAllJudges(): Promise<Judge[]> {
|
||||
const sql = 'SELECT * FROM judges WHERE is_active = TRUE ORDER BY created_at DESC';
|
||||
const results = await db.query<Judge>(sql);
|
||||
return results.map(judge => ({
|
||||
...judge,
|
||||
expertise: JSON.parse(judge.expertise as any)
|
||||
}));
|
||||
}
|
||||
|
||||
// 更新評審
|
||||
static async updateJudge(id: string, updates: Partial<Judge>): Promise<boolean> {
|
||||
const fields = Object.keys(updates).filter(key => key !== 'id' && key !== 'created_at');
|
||||
const setClause = fields.map(field => `${field} = ?`).join(', ');
|
||||
const values = fields.map(field => {
|
||||
if (field === 'expertise') {
|
||||
return JSON.stringify((updates as any)[field]);
|
||||
}
|
||||
return (updates as any)[field];
|
||||
});
|
||||
|
||||
const sql = `UPDATE judges SET ${setClause}, updated_at = CURRENT_TIMESTAMP WHERE id = ?`;
|
||||
const result = await db.update(sql, [...values, id]);
|
||||
return result.affectedRows > 0;
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// 競賽服務
|
||||
// =====================================================
|
||||
export class CompetitionService {
|
||||
// 創建競賽
|
||||
static async createCompetition(competitionData: Omit<Competition, 'id' | 'created_at' | 'updated_at'>): Promise<Competition> {
|
||||
const sql = `
|
||||
INSERT INTO competitions (id, name, year, month, start_date, end_date, status, description, type, evaluation_focus, max_team_size, is_active)
|
||||
VALUES (UUID(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`;
|
||||
const params = [
|
||||
competitionData.name,
|
||||
competitionData.year,
|
||||
competitionData.month,
|
||||
competitionData.start_date,
|
||||
competitionData.end_date,
|
||||
competitionData.status,
|
||||
competitionData.description || null,
|
||||
competitionData.type,
|
||||
competitionData.evaluation_focus || null,
|
||||
competitionData.max_team_size || null,
|
||||
competitionData.is_active
|
||||
];
|
||||
|
||||
await db.insert(sql, params);
|
||||
return await this.getCompetitionByName(competitionData.name) as Competition;
|
||||
}
|
||||
|
||||
// 根據名稱獲取競賽
|
||||
static async getCompetitionByName(name: string): Promise<Competition | null> {
|
||||
const sql = 'SELECT * FROM competitions WHERE name = ? AND is_active = TRUE';
|
||||
return await db.queryOne<Competition>(sql, [name]);
|
||||
}
|
||||
|
||||
// 根據ID獲取競賽
|
||||
static async getCompetitionById(id: string): Promise<Competition | null> {
|
||||
const sql = 'SELECT * FROM competitions WHERE id = ? AND is_active = TRUE';
|
||||
return await db.queryOne<Competition>(sql, [id]);
|
||||
}
|
||||
|
||||
// 獲取所有競賽
|
||||
static async getAllCompetitions(): Promise<Competition[]> {
|
||||
const sql = 'SELECT * FROM competitions WHERE is_active = TRUE ORDER BY year DESC, month DESC';
|
||||
return await db.query<Competition>(sql);
|
||||
}
|
||||
|
||||
// 獲取競賽統計
|
||||
static async getCompetitionStatistics(id: string): Promise<CompetitionStatistics | null> {
|
||||
const sql = 'SELECT * FROM competition_statistics WHERE id = ?';
|
||||
return await db.queryOne<CompetitionStatistics>(sql, [id]);
|
||||
}
|
||||
|
||||
// 更新競賽
|
||||
static async updateCompetition(id: string, updates: Partial<Competition>): Promise<boolean> {
|
||||
const fields = Object.keys(updates).filter(key => key !== 'id' && key !== 'created_at');
|
||||
const setClause = fields.map(field => `${field} = ?`).join(', ');
|
||||
const values = fields.map(field => (updates as any)[field]);
|
||||
|
||||
const sql = `UPDATE competitions SET ${setClause}, updated_at = CURRENT_TIMESTAMP WHERE id = ?`;
|
||||
const result = await db.update(sql, [...values, id]);
|
||||
return result.affectedRows > 0;
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// 應用服務
|
||||
// =====================================================
|
||||
export class AppService {
|
||||
// 創建應用
|
||||
static async createApp(appData: Omit<App, 'id' | 'created_at' | 'updated_at'>): Promise<App> {
|
||||
const sql = `
|
||||
INSERT INTO apps (id, name, description, creator_id, team_id, category, type, likes_count, views_count, rating, is_active)
|
||||
VALUES (UUID(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`;
|
||||
const params = [
|
||||
appData.name,
|
||||
appData.description || null,
|
||||
appData.creator_id,
|
||||
appData.team_id || null,
|
||||
appData.category,
|
||||
appData.type,
|
||||
appData.likes_count,
|
||||
appData.views_count,
|
||||
appData.rating,
|
||||
appData.is_active
|
||||
];
|
||||
|
||||
await db.insert(sql, params);
|
||||
return await this.getAppByName(appData.name) as App;
|
||||
}
|
||||
|
||||
// 根據名稱獲取應用
|
||||
static async getAppByName(name: string): Promise<App | null> {
|
||||
const sql = 'SELECT * FROM apps WHERE name = ? AND is_active = TRUE';
|
||||
return await db.queryOne<App>(sql, [name]);
|
||||
}
|
||||
|
||||
// 根據ID獲取應用
|
||||
static async getAppById(id: string): Promise<App | null> {
|
||||
const sql = 'SELECT * FROM apps WHERE id = ? AND is_active = TRUE';
|
||||
return await db.queryOne<App>(sql, [id]);
|
||||
}
|
||||
|
||||
// 獲取所有應用
|
||||
static async getAllApps(limit = 50, offset = 0): Promise<App[]> {
|
||||
const sql = 'SELECT * FROM apps WHERE is_active = TRUE ORDER BY created_at DESC LIMIT ? OFFSET ?';
|
||||
return await db.query<App>(sql, [limit, offset]);
|
||||
}
|
||||
|
||||
// 獲取應用統計
|
||||
static async getAppStatistics(id: string): Promise<AppStatistics | null> {
|
||||
const sql = 'SELECT * FROM app_statistics WHERE id = ?';
|
||||
return await db.queryOne<AppStatistics>(sql, [id]);
|
||||
}
|
||||
|
||||
// 更新應用
|
||||
static async updateApp(id: string, updates: Partial<App>): Promise<boolean> {
|
||||
const fields = Object.keys(updates).filter(key => key !== 'id' && key !== 'created_at');
|
||||
const setClause = fields.map(field => `${field} = ?`).join(', ');
|
||||
const values = fields.map(field => (updates as any)[field]);
|
||||
|
||||
const sql = `UPDATE apps SET ${setClause}, updated_at = CURRENT_TIMESTAMP WHERE id = ?`;
|
||||
const result = await db.update(sql, [...values, id]);
|
||||
return result.affectedRows > 0;
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// 評分服務
|
||||
// =====================================================
|
||||
export class ScoringService {
|
||||
// 提交應用評分
|
||||
static async submitAppScore(scoreData: Omit<AppJudgeScore, 'id' | 'submitted_at'>): Promise<AppJudgeScore> {
|
||||
const sql = `
|
||||
INSERT INTO app_judge_scores (id, judge_id, app_id, innovation_score, technical_score, usability_score, presentation_score, impact_score, total_score, comments)
|
||||
VALUES (UUID(), ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
innovation_score = VALUES(innovation_score),
|
||||
technical_score = VALUES(technical_score),
|
||||
usability_score = VALUES(usability_score),
|
||||
presentation_score = VALUES(presentation_score),
|
||||
impact_score = VALUES(impact_score),
|
||||
total_score = VALUES(total_score),
|
||||
comments = VALUES(comments),
|
||||
submitted_at = CURRENT_TIMESTAMP
|
||||
`;
|
||||
const params = [
|
||||
scoreData.judge_id,
|
||||
scoreData.app_id,
|
||||
scoreData.innovation_score,
|
||||
scoreData.technical_score,
|
||||
scoreData.usability_score,
|
||||
scoreData.presentation_score,
|
||||
scoreData.impact_score,
|
||||
scoreData.total_score,
|
||||
scoreData.comments || null
|
||||
];
|
||||
|
||||
await db.insert(sql, params);
|
||||
return await this.getAppScore(scoreData.judge_id, scoreData.app_id) as AppJudgeScore;
|
||||
}
|
||||
|
||||
// 獲取應用評分
|
||||
static async getAppScore(judgeId: string, appId: string): Promise<AppJudgeScore | null> {
|
||||
const sql = 'SELECT * FROM app_judge_scores WHERE judge_id = ? AND app_id = ?';
|
||||
return await db.queryOne<AppJudgeScore>(sql, [judgeId, appId]);
|
||||
}
|
||||
|
||||
// 獲取應用的所有評分
|
||||
static async getAppScores(appId: string): Promise<AppJudgeScore[]> {
|
||||
const sql = 'SELECT * FROM app_judge_scores WHERE app_id = ? ORDER BY submitted_at DESC';
|
||||
return await db.query<AppJudgeScore>(sql, [appId]);
|
||||
}
|
||||
|
||||
// 提交提案評分
|
||||
static async submitProposalScore(scoreData: Omit<ProposalJudgeScore, 'id' | 'submitted_at'>): Promise<ProposalJudgeScore> {
|
||||
const sql = `
|
||||
INSERT INTO proposal_judge_scores (id, judge_id, proposal_id, problem_identification_score, solution_feasibility_score, innovation_score, impact_score, presentation_score, total_score, comments)
|
||||
VALUES (UUID(), ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
problem_identification_score = VALUES(problem_identification_score),
|
||||
solution_feasibility_score = VALUES(solution_feasibility_score),
|
||||
innovation_score = VALUES(innovation_score),
|
||||
impact_score = VALUES(impact_score),
|
||||
presentation_score = VALUES(presentation_score),
|
||||
total_score = VALUES(total_score),
|
||||
comments = VALUES(comments),
|
||||
submitted_at = CURRENT_TIMESTAMP
|
||||
`;
|
||||
const params = [
|
||||
scoreData.judge_id,
|
||||
scoreData.proposal_id,
|
||||
scoreData.problem_identification_score,
|
||||
scoreData.solution_feasibility_score,
|
||||
scoreData.innovation_score,
|
||||
scoreData.impact_score,
|
||||
scoreData.presentation_score,
|
||||
scoreData.total_score,
|
||||
scoreData.comments || null
|
||||
];
|
||||
|
||||
await db.insert(sql, params);
|
||||
return await this.getProposalScore(scoreData.judge_id, scoreData.proposal_id) as ProposalJudgeScore;
|
||||
}
|
||||
|
||||
// 獲取提案評分
|
||||
static async getProposalScore(judgeId: string, proposalId: string): Promise<ProposalJudgeScore | null> {
|
||||
const sql = 'SELECT * FROM proposal_judge_scores WHERE judge_id = ? AND proposal_id = ?';
|
||||
return await db.queryOne<ProposalJudgeScore>(sql, [judgeId, proposalId]);
|
||||
}
|
||||
|
||||
// 獲取提案的所有評分
|
||||
static async getProposalScores(proposalId: string): Promise<ProposalJudgeScore[]> {
|
||||
const sql = 'SELECT * FROM proposal_judge_scores WHERE proposal_id = ? ORDER BY submitted_at DESC';
|
||||
return await db.query<ProposalJudgeScore>(sql, [proposalId]);
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// 獎項服務
|
||||
// =====================================================
|
||||
export class AwardService {
|
||||
// 創建獎項
|
||||
static async createAward(awardData: Omit<Award, 'id' | 'created_at'>): Promise<Award> {
|
||||
const sql = `
|
||||
INSERT INTO awards (id, competition_id, app_id, team_id, proposal_id, app_name, team_name, proposal_title, creator, award_type, award_name, score, year, month, icon, custom_award_type_id, competition_type, rank, category)
|
||||
VALUES (UUID(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`;
|
||||
const params = [
|
||||
awardData.competition_id,
|
||||
awardData.app_id || null,
|
||||
awardData.team_id || null,
|
||||
awardData.proposal_id || null,
|
||||
awardData.app_name || null,
|
||||
awardData.team_name || null,
|
||||
awardData.proposal_title || null,
|
||||
awardData.creator,
|
||||
awardData.award_type,
|
||||
awardData.award_name,
|
||||
awardData.score,
|
||||
awardData.year,
|
||||
awardData.month,
|
||||
awardData.icon,
|
||||
awardData.custom_award_type_id || null,
|
||||
awardData.competition_type,
|
||||
awardData.rank,
|
||||
awardData.category
|
||||
];
|
||||
|
||||
await db.insert(sql, params);
|
||||
return await this.getAwardByCompetitionAndCreator(awardData.competition_id, awardData.creator) as Award;
|
||||
}
|
||||
|
||||
// 根據競賽和創作者獲取獎項
|
||||
static async getAwardByCompetitionAndCreator(competitionId: string, creator: string): Promise<Award | null> {
|
||||
const sql = 'SELECT * FROM awards WHERE competition_id = ? AND creator = ? ORDER BY created_at DESC LIMIT 1';
|
||||
return await db.queryOne<Award>(sql, [competitionId, creator]);
|
||||
}
|
||||
|
||||
// 根據年份獲取獎項
|
||||
static async getAwardsByYear(year: number): Promise<Award[]> {
|
||||
const sql = 'SELECT * FROM awards WHERE year = ? ORDER BY month DESC, rank ASC';
|
||||
return await db.query<Award>(sql, [year]);
|
||||
}
|
||||
|
||||
// 根據競賽獲取獎項
|
||||
static async getAwardsByCompetition(competitionId: string): Promise<Award[]> {
|
||||
const sql = 'SELECT * FROM awards WHERE competition_id = ? ORDER BY rank ASC';
|
||||
return await db.query<Award>(sql, [competitionId]);
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// 系統設定服務
|
||||
// =====================================================
|
||||
export class SystemSettingService {
|
||||
// 獲取設定值
|
||||
static async getSetting(key: string): Promise<string | null> {
|
||||
const sql = 'SELECT value FROM system_settings WHERE `key` = ?';
|
||||
const result = await db.queryOne<{ value: string }>(sql, [key]);
|
||||
return result?.value || null;
|
||||
}
|
||||
|
||||
// 設定值
|
||||
static async setSetting(key: string, value: string, description?: string, category = 'general', isPublic = false): Promise<boolean> {
|
||||
const sql = `
|
||||
INSERT INTO system_settings (id, \`key\`, value, description, category, is_public)
|
||||
VALUES (UUID(), ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
value = VALUES(value),
|
||||
description = VALUES(description),
|
||||
category = VALUES(category),
|
||||
is_public = VALUES(is_public),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
`;
|
||||
const params = [key, value, description || null, category, isPublic];
|
||||
|
||||
const result = await db.insert(sql, params);
|
||||
return result.affectedRows > 0;
|
||||
}
|
||||
|
||||
// 獲取所有公開設定
|
||||
static async getPublicSettings(): Promise<SystemSetting[]> {
|
||||
const sql = 'SELECT * FROM system_settings WHERE is_public = TRUE ORDER BY category, `key`';
|
||||
return await db.query<SystemSetting>(sql);
|
||||
}
|
||||
}
|
101
lib/services/email-service.ts
Normal file
101
lib/services/email-service.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
// =====================================================
|
||||
// 郵件服務
|
||||
// =====================================================
|
||||
|
||||
import nodemailer from 'nodemailer';
|
||||
|
||||
// 郵件配置
|
||||
const emailConfig = {
|
||||
host: process.env.SMTP_HOST || 'smtp.gmail.com',
|
||||
port: parseInt(process.env.SMTP_PORT || '587'),
|
||||
secure: false, // true for 465, false for other ports
|
||||
auth: {
|
||||
user: process.env.SMTP_USER,
|
||||
pass: process.env.SMTP_PASS,
|
||||
},
|
||||
};
|
||||
|
||||
// 創建郵件傳輸器
|
||||
const transporter = nodemailer.createTransport(emailConfig);
|
||||
|
||||
export class EmailService {
|
||||
// 發送密碼重設郵件
|
||||
static async sendPasswordResetEmail(email: string, resetToken: string, userName: string) {
|
||||
try {
|
||||
const resetUrl = `${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/reset-password?token=${resetToken}`;
|
||||
|
||||
const mailOptions = {
|
||||
from: `"${process.env.NEXT_PUBLIC_APP_NAME || 'AI 展示平台'}" <${emailConfig.auth.user}>`,
|
||||
to: email,
|
||||
subject: '密碼重設請求',
|
||||
html: `
|
||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||||
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 30px; text-align: center;">
|
||||
<h1 style="color: white; margin: 0; font-size: 24px;">密碼重設請求</h1>
|
||||
</div>
|
||||
|
||||
<div style="padding: 30px; background: #f9f9f9;">
|
||||
<h2 style="color: #333; margin-top: 0;">親愛的 ${userName},</h2>
|
||||
|
||||
<p style="color: #666; line-height: 1.6;">
|
||||
我們收到了您的密碼重設請求。請點擊下方按鈕來重設您的密碼:
|
||||
</p>
|
||||
|
||||
<div style="text-align: center; margin: 30px 0;">
|
||||
<a href="${resetUrl}"
|
||||
style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 15px 30px;
|
||||
text-decoration: none;
|
||||
border-radius: 5px;
|
||||
display: inline-block;
|
||||
font-weight: bold;">
|
||||
重設密碼
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p style="color: #666; line-height: 1.6; font-size: 14px;">
|
||||
如果按鈕無法點擊,請複製以下連結到瀏覽器中:<br>
|
||||
<a href="${resetUrl}" style="color: #667eea; word-break: break-all;">${resetUrl}</a>
|
||||
</p>
|
||||
|
||||
<div style="background: #fff3cd; border: 1px solid #ffeaa7; padding: 15px; border-radius: 5px; margin: 20px 0;">
|
||||
<p style="color: #856404; margin: 0; font-size: 14px;">
|
||||
<strong>注意:</strong>此連結將在 1 小時後過期,請盡快完成密碼重設。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p style="color: #666; line-height: 1.6; font-size: 14px;">
|
||||
如果您沒有請求密碼重設,請忽略此郵件。您的密碼將保持不變。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="background: #333; color: white; padding: 20px; text-align: center; font-size: 12px;">
|
||||
<p style="margin: 0;">此郵件由 ${process.env.NEXT_PUBLIC_APP_NAME || 'AI 展示平台'} 系統自動發送</p>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
};
|
||||
|
||||
const result = await transporter.sendMail(mailOptions);
|
||||
console.log('密碼重設郵件發送成功:', result.messageId);
|
||||
return { success: true, messageId: result.messageId };
|
||||
|
||||
} catch (error) {
|
||||
console.error('發送密碼重設郵件失敗:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
// 測試郵件配置
|
||||
static async testEmailConfig() {
|
||||
try {
|
||||
await transporter.verify();
|
||||
console.log('郵件配置驗證成功');
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('郵件配置驗證失敗:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
}
|
131
lib/services/password-reset-service.ts
Normal file
131
lib/services/password-reset-service.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
// =====================================================
|
||||
// 密碼重設服務
|
||||
// =====================================================
|
||||
|
||||
import { db } from '../database';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import bcrypt from 'bcryptjs';
|
||||
|
||||
export interface PasswordResetToken {
|
||||
id: string;
|
||||
user_id: string;
|
||||
token: string;
|
||||
expires_at: string;
|
||||
used_at?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export class PasswordResetService {
|
||||
// 創建密碼重設 token
|
||||
static async createResetToken(userId: string): Promise<PasswordResetToken> {
|
||||
const token = uuidv4() + '-' + Date.now();
|
||||
const expiresAt = new Date(Date.now() + 60 * 60 * 1000); // 1 小時後過期
|
||||
|
||||
const sql = `
|
||||
INSERT INTO password_reset_tokens (id, user_id, token, expires_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`;
|
||||
|
||||
const params = [uuidv4(), userId, token, expiresAt];
|
||||
await db.insert(sql, params);
|
||||
|
||||
return {
|
||||
id: params[0],
|
||||
user_id: userId,
|
||||
token,
|
||||
expires_at: expiresAt.toISOString(),
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
// 驗證密碼重設 token
|
||||
static async validateResetToken(token: string): Promise<PasswordResetToken | null> {
|
||||
const sql = `
|
||||
SELECT * FROM password_reset_tokens
|
||||
WHERE token = ? AND expires_at > NOW() AND used_at IS NULL
|
||||
`;
|
||||
|
||||
const result = await db.queryOne<PasswordResetToken>(sql, [token]);
|
||||
return result;
|
||||
}
|
||||
|
||||
// 使用密碼重設 token
|
||||
static async useResetToken(token: string, newPassword: string): Promise<boolean> {
|
||||
const connection = await db.getConnection();
|
||||
|
||||
try {
|
||||
await connection.beginTransaction();
|
||||
|
||||
// 獲取 token 信息
|
||||
const tokenInfo = await this.validateResetToken(token);
|
||||
if (!tokenInfo) {
|
||||
throw new Error('無效或已過期的重設 token');
|
||||
}
|
||||
|
||||
// 加密新密碼
|
||||
const saltRounds = 12;
|
||||
const passwordHash = await bcrypt.hash(newPassword, saltRounds);
|
||||
|
||||
// 更新用戶密碼
|
||||
const updateUserSql = `
|
||||
UPDATE users
|
||||
SET password_hash = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
`;
|
||||
await connection.execute(updateUserSql, [passwordHash, tokenInfo.user_id]);
|
||||
|
||||
// 標記 token 為已使用
|
||||
const updateTokenSql = `
|
||||
UPDATE password_reset_tokens
|
||||
SET used_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE token = ?
|
||||
`;
|
||||
await connection.execute(updateTokenSql, [token]);
|
||||
|
||||
await connection.commit();
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
await connection.rollback();
|
||||
throw error;
|
||||
} finally {
|
||||
connection.release();
|
||||
}
|
||||
}
|
||||
|
||||
// 清理過期的 tokens
|
||||
static async cleanupExpiredTokens(): Promise<number> {
|
||||
const sql = `
|
||||
DELETE FROM password_reset_tokens
|
||||
WHERE expires_at < NOW() OR used_at IS NOT NULL
|
||||
`;
|
||||
|
||||
const result = await db.update(sql, []);
|
||||
return result.affectedRows;
|
||||
}
|
||||
|
||||
// 獲取用戶的活躍重設 tokens
|
||||
static async getUserActiveTokens(userId: string): Promise<PasswordResetToken[]> {
|
||||
const sql = `
|
||||
SELECT * FROM password_reset_tokens
|
||||
WHERE user_id = ? AND expires_at > NOW() AND used_at IS NULL
|
||||
ORDER BY created_at DESC
|
||||
`;
|
||||
|
||||
return await db.query<PasswordResetToken>(sql, [userId]);
|
||||
}
|
||||
|
||||
// 撤銷用戶的所有重設 tokens
|
||||
static async revokeUserTokens(userId: string): Promise<number> {
|
||||
const sql = `
|
||||
UPDATE password_reset_tokens
|
||||
SET used_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE user_id = ? AND used_at IS NULL
|
||||
`;
|
||||
|
||||
const result = await db.update(sql, [userId]);
|
||||
return result.affectedRows;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user