// ===================================================== // 資料庫服務層 // ===================================================== import { db } from '../database'; import bcrypt from 'bcryptjs'; import crypto from 'crypto'; 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): Promise { const sql = ` INSERT INTO users (id, name, email, password_hash, avatar, department, role, join_date, total_likes, total_views, status, 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.status, userData.last_login || null ]; await db.insert(sql, params); return await this.findByEmail(userData.email) as User; } // 根據郵箱獲取用戶 async findByEmail(email: string): Promise { const sql = 'SELECT * FROM users WHERE email = ? AND status = "active"'; return await db.queryOne(sql, [email]); } // 根據ID獲取用戶 async findById(id: string): Promise { const sql = 'SELECT * FROM users WHERE id = ? AND status = "active"'; return await db.queryOne(sql, [id]); } // 更新用戶 async update(id: string, updates: Partial): Promise { 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 { 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 { const sql = 'SELECT * FROM user_statistics WHERE id = ?'; return await db.queryOne(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: string[] = []; 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('status = ? AND last_login IS NOT NULL AND last_login >= DATE_SUB(NOW(), INTERVAL 30 DAY)'); params.push('active'); } else if (status === 'inactive') { whereConditions.push('status = ?'); params.push('inactive'); } else if (status === 'invited') { whereConditions.push('status = ?'); params.push('invited'); } } 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, status, last_login, created_at, updated_at FROM users ${whereClause} ORDER BY created_at DESC LIMIT ${offset}, ${limit} `; const users = await this.query(usersSql, params); return { users, total }; } // 獲取用戶統計數據 async getUserStats(): Promise<{ totalUsers: number; activeUsers: number; adminCount: number; developerCount: number; invitedUsers: number; inactiveUsers: number; newThisMonth: number; }> { const sql = ` SELECT COUNT(*) as total_users, COUNT(CASE WHEN status = 'active' AND last_login IS NOT NULL AND last_login >= DATE_SUB(CURDATE(), 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 status = 'invited' THEN 1 END) as invited_users, COUNT(CASE WHEN status = 'inactive' THEN 1 END) as inactive_users, COUNT(CASE WHEN created_at >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) THEN 1 END) as new_this_month FROM users `; 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, invitedUsers: stats.invited_users || 0, inactiveUsers: stats.inactive_users || 0, newThisMonth: stats.new_this_month || 0 }; } // 獲取用戶活動記錄 async getUserActivities( userId: string, page: number = 1, limit: number = 10, filters: { search?: string; startDate?: string; endDate?: string; activityType?: string; } = {} ): Promise<{ activities: any[]; pagination: any }> { // 構建篩選條件 let whereConditions = []; let dateFilter = ''; // 日期篩選 if (filters.startDate && filters.endDate) { dateFilter = `AND created_at BETWEEN '${filters.startDate} 00:00:00' AND '${filters.endDate} 23:59:59'`; } else if (filters.startDate) { dateFilter = `AND created_at >= '${filters.startDate} 00:00:00'`; } else if (filters.endDate) { dateFilter = `AND created_at <= '${filters.endDate} 23:59:59'`; } // 活動類型篩選 let activityTypeFilter = ''; if (filters.activityType && filters.activityType !== 'all') { activityTypeFilter = `AND activity_type = '${filters.activityType}'`; } // 搜尋篩選 let searchFilter = ''; if (filters.search) { searchFilter = `AND action_text LIKE '%${filters.search}%'`; } // 使用字符串插值避免 UNION ALL 參數化查詢問題 const baseSql = ` SELECT 'login' as activity_type, '登入系統' as action_text, 'Calendar' as icon, 'blue' as color, u.last_login as created_at, NULL as app_name, NULL as app_id FROM users u WHERE u.id = '${userId}' AND u.last_login IS NOT NULL UNION ALL SELECT 'favorite' as activity_type, CONCAT('收藏應用:', a.name) as action_text, 'Heart' as icon, 'red' as color, uf.created_at, a.name as app_name, a.id as app_id FROM user_favorites uf JOIN apps a ON uf.app_id = a.id WHERE uf.user_id = '${userId}' UNION ALL SELECT 'like' as activity_type, CONCAT('按讚應用:', a.name) as action_text, 'ThumbsUp' as icon, 'green' as color, ul.liked_at as created_at, a.name as app_name, a.id as app_id FROM user_likes ul JOIN apps a ON ul.app_id = a.id WHERE ul.user_id = '${userId}' UNION ALL SELECT 'view' as activity_type, CONCAT('查看應用:', a.name) as action_text, 'Eye' as icon, 'purple' as color, uv.viewed_at as created_at, a.name as app_name, a.id as app_id FROM user_views uv JOIN apps a ON uv.app_id = a.id WHERE uv.user_id = '${userId}' UNION ALL SELECT 'rating' as activity_type, CONCAT('評價應用:', a.name, ' (', ur.rating, '分)') as action_text, 'Star' as icon, 'yellow' as color, ur.rated_at as created_at, a.name as app_name, a.id as app_id FROM user_ratings ur JOIN apps a ON ur.app_id = a.id WHERE ur.user_id = '${userId}' UNION ALL SELECT 'create' as activity_type, CONCAT('創建應用:', a.name) as action_text, 'Plus' as icon, 'blue' as color, a.created_at, a.name as app_name, a.id as app_id FROM apps a WHERE a.creator_id = '${userId}' `; // 先獲取總數 const countSql = ` SELECT COUNT(*) as total FROM ( ${baseSql} ) as activities WHERE 1=1 ${dateFilter} ${activityTypeFilter} ${searchFilter} `; const countResult = await this.query(countSql, []); const total = countResult[0]?.total || 0; const totalPages = Math.ceil(total / limit); const offset = (page - 1) * limit; // 獲取分頁數據 const dataSql = ` SELECT * FROM ( ${baseSql} ) as activities WHERE 1=1 ${dateFilter} ${activityTypeFilter} ${searchFilter} ORDER BY created_at DESC LIMIT ${limit} OFFSET ${offset} `; const activities = await this.query(dataSql, []); return { activities, pagination: { page, limit, total, totalPages } }; } // 獲取用戶詳細統計數據 async getUserDetailedStats(userId: string): Promise { // 獲取創建應用數量 const appsSql = ` SELECT COUNT(*) as total_apps FROM apps WHERE creator_id = '${userId}' AND is_active = TRUE `; const appsResult = await this.query(appsSql, []); const totalApps = appsResult[0]?.total_apps || 0; // 獲取撰寫評價數量 const reviewsSql = ` SELECT COUNT(*) as total_reviews FROM user_ratings WHERE user_id = '${userId}' `; const reviewsResult = await this.query(reviewsSql, []); const totalReviews = reviewsResult[0]?.total_reviews || 0; // 獲取獲得讚數(用戶創建的應用獲得的總讚數) const likesSql = ` SELECT COALESCE(SUM(a.likes_count), 0) as total_likes FROM apps a WHERE a.creator_id = '${userId}' AND a.is_active = TRUE `; const likesResult = await this.query(likesSql, []); const totalLikes = likesResult[0]?.total_likes || 0; // 獲取登入天數(從註冊日期到現在的天數) const loginDaysSql = ` SELECT DATEDIFF(CURDATE(), join_date) as login_days FROM users WHERE id = '${userId}' `; const loginDaysResult = await this.query(loginDaysSql, []); const loginDays = loginDaysResult[0]?.login_days || 0; return { totalApps, totalReviews, totalLikes, loginDays }; } // 更新用戶資料 async updateUser(userId: string, userData: { name: string; department: string; role: string; status: string; }): Promise<{ success: boolean; user?: any; error?: string }> { try { const sql = ` UPDATE users SET name = ?, department = ?, role = ?, status = ?, updated_at = NOW() WHERE id = ? `; await this.query(sql, [ userData.name, userData.department, userData.role, userData.status, userId ]); // 獲取更新後的用戶資料 const updatedUserSql = ` SELECT id, name, email, department, role, status, join_date, last_login, total_likes, total_views, created_at, updated_at FROM users WHERE id = ? `; const updatedUser = await this.queryOne(updatedUserSql, [userId]); if (!updatedUser) { return { success: false, error: '用戶不存在' }; } return { success: true, user: updatedUser }; } catch (error) { console.error('更新用戶錯誤:', error); return { success: false, error: '更新用戶時發生錯誤' }; } } // 刪除用戶 async deleteUser(userId: string): Promise<{ success: boolean; error?: string }> { try { // 檢查用戶是否存在 const checkUserSql = `SELECT id FROM users WHERE id = ?`; const user = await this.queryOne(checkUserSql, [userId]); if (!user) { return { success: false, error: '用戶不存在' }; } // 刪除用戶(由於外鍵約束,相關的活動記錄也會被自動刪除) const deleteSql = `DELETE FROM users WHERE id = ?`; await this.query(deleteSql, [userId]); return { success: true }; } catch (error) { console.error('刪除用戶錯誤:', error); return { success: false, error: '刪除用戶時發生錯誤' }; } } // 創建邀請用戶 async createInvitedUser(email: string, role: string): Promise<{ success: boolean; user?: any; invitationLink?: string; error?: string }> { try { // 生成邀請 token const invitationToken = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15) // 創建邀請用戶記錄 const userId = crypto.randomUUID() const sql = ` INSERT INTO users (id, name, email, password_hash, department, role, status, join_date, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, CURDATE(), NOW(), NOW()) `; // 為邀請用戶創建一個臨時密碼(用戶註冊時會覆蓋) const tempPassword = 'temp_' + Math.random().toString(36).substring(2, 15) const tempPasswordHash = await bcrypt.hash(tempPassword, 10) await this.query(sql, [ userId, '', // 姓名留空,用戶註冊時填寫 email, tempPasswordHash, '', // 部門留空,用戶註冊時填寫 role, 'invited' ]); // 獲取創建的用戶資料 const userSql = ` SELECT id, name, email, department, role, status, join_date, created_at, updated_at FROM users WHERE id = ? `; const user = await this.queryOne(userSql, [userId]) if (!user) { return { success: false, error: '創建邀請用戶失敗' } } // 生成邀請連結 const invitationLink = `${process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000'}/register?token=${invitationToken}&email=${encodeURIComponent(email)}&role=${role}` return { success: true, user: { ...user, invitationToken, invitationLink }, invitationLink } } catch (error) { console.error('創建邀請用戶錯誤:', error); return { success: false, error: '創建邀請用戶時發生錯誤' }; } } // 查找邀請用戶(狀態為 invited) async findInvitedUserByEmail(email: string): Promise { const sql = 'SELECT * FROM users WHERE email = ? AND status = "invited"'; return await this.queryOne(sql, [email]); } // 完成邀請用戶註冊 async completeInvitedUserRegistration( userId: string, name: string, department: string, passwordHash: string, role: string ): Promise<{ success: boolean; user?: any; error?: string }> { try { const sql = ` UPDATE users SET name = ?, department = ?, password_hash = ?, role = ?, status = 'active', updated_at = NOW() WHERE id = ? AND status = 'invited' `; await this.query(sql, [name, department, passwordHash, role, userId]); // 獲取更新後的用戶資料 const updatedUserSql = ` SELECT id, name, email, department, role, status, join_date, last_login, total_likes, total_views, created_at, updated_at FROM users WHERE id = ? `; const updatedUser = await this.queryOne(updatedUserSql, [userId]); if (!updatedUser) { return { success: false, error: '用戶不存在或狀態不正確' }; } return { success: true, user: updatedUser }; } catch (error) { console.error('完成邀請用戶註冊錯誤:', error); return { success: false, error: '完成註冊時發生錯誤' }; } } // 通用查詢方法 async query(sql: string, params: any[] = []): Promise { return await db.query(sql, params); } // 獲取儀表板統計數據 async getDashboardStats(): Promise<{ totalUsers: number; activeUsers: number; totalApps: number; totalCompetitions: number; totalReviews: number; totalViews: number; totalLikes: number; newAppsThisMonth: number; activeCompetitions: number; growthRate: number; }> { try { // 用戶統計 const userStats = await this.getUserStats(); // 應用統計 const appStatsSql = ` SELECT COUNT(*) as total_apps, COUNT(CASE WHEN created_at >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) THEN 1 END) as new_apps_this_month, COALESCE(SUM(views_count), 0) as total_views, COALESCE(SUM(likes_count), 0) as total_likes FROM apps WHERE is_active = TRUE `; const appStats = await this.queryOne(appStatsSql); // 評論統計 const reviewStatsSql = ` SELECT COUNT(*) as total_reviews FROM user_ratings `; const reviewStats = await this.queryOne(reviewStatsSql); // 競賽統計 const competitionStatsSql = ` SELECT COUNT(*) as total_competitions, COUNT(CASE WHEN status = 'active' THEN 1 END) as active_competitions FROM competitions `; const competitionStats = await this.queryOne(competitionStatsSql); // 計算增長率(與上個月比較) const lastMonthUsersSql = ` SELECT COUNT(*) as last_month_users FROM users WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL 60 DAY) AND created_at < DATE_SUB(CURDATE(), INTERVAL 30 DAY) `; const lastMonthUsers = await this.queryOne(lastMonthUsersSql); const currentMonthUsers = userStats.newThisMonth; const growthRate = lastMonthUsers?.last_month_users > 0 ? Math.round(((currentMonthUsers - lastMonthUsers.last_month_users) / lastMonthUsers.last_month_users) * 100) : 0; return { totalUsers: userStats.totalUsers, activeUsers: userStats.activeUsers, totalApps: appStats?.total_apps || 0, totalCompetitions: competitionStats?.total_competitions || 0, totalReviews: reviewStats?.total_reviews || 0, totalViews: appStats?.total_views || 0, totalLikes: appStats?.total_likes || 0, newAppsThisMonth: appStats?.new_apps_this_month || 0, activeCompetitions: competitionStats?.active_competitions || 0, growthRate: growthRate }; } catch (error) { console.error('獲取儀表板統計數據錯誤:', error); return { totalUsers: 0, activeUsers: 0, totalApps: 0, totalCompetitions: 0, totalReviews: 0, totalViews: 0, totalLikes: 0, newAppsThisMonth: 0, activeCompetitions: 0, growthRate: 0 }; } } // 獲取最新活動 async getRecentActivities(limit: number = 10): Promise { try { const sql = ` SELECT 'user_register' as activity_type, '用戶註冊' as activity_name, CONCAT(name, ' 註冊了平台') as description, created_at as activity_time, 'user' as icon_type, 'blue' as color FROM users WHERE status = 'active' ORDER BY created_at DESC LIMIT ${limit} `; const activities = await this.query(sql); return activities.map(activity => ({ id: `user_${activity.activity_time}`, type: activity.activity_type, message: activity.description, time: new Date(activity.activity_time).toLocaleString('zh-TW'), icon: 'Users', color: 'text-blue-600' })); } catch (error) { console.error('獲取最新活動錯誤:', error); return []; } } // 獲取熱門應用 async getTopApps(limit: number = 5): Promise { try { const sql = ` SELECT a.id, a.name, a.description, a.views_count as views, a.likes_count as likes, COALESCE(AVG(ur.rating), 0) as rating, a.category, a.created_at FROM apps a LEFT JOIN user_ratings ur ON a.id = ur.app_id WHERE a.is_active = TRUE GROUP BY a.id, a.name, a.description, a.views_count, a.likes_count, a.category, a.created_at ORDER BY (a.views_count + a.likes_count * 2) DESC LIMIT ${limit} `; const apps = await this.query(sql); return apps.map(app => ({ id: app.id, name: app.name, description: app.description, views: app.views || 0, likes: app.likes || 0, rating: Math.round(app.rating * 10) / 10, category: app.category, created_at: app.created_at })); } catch (error) { console.error('獲取熱門應用錯誤:', error); return []; } } // 通用單一查詢方法 async queryOne(sql: string, params: any[] = []): Promise { return await db.queryOne(sql, params); } // 獲取所有用戶 async getAllUsers(limit = 50, offset = 0): Promise { const sql = 'SELECT * FROM users WHERE is_active = TRUE ORDER BY created_at DESC LIMIT ? OFFSET ?'; return await db.query(sql, [limit, offset]); } // 靜態方法保持向後兼容 static async createUser(userData: Omit): Promise { const service = new UserService(); return await service.create(userData); } static async getUserByEmail(email: string): Promise { const service = new UserService(); return await service.findByEmail(email); } static async getUserById(id: string): Promise { const service = new UserService(); return await service.findById(id); } static async updateUser(id: string, updates: Partial): Promise { const service = new UserService(); const result = await service.update(id, updates); return result !== null; } static async getUserStatistics(id: string): Promise { const service = new UserService(); return await service.getUserStatistics(id); } static async getAllUsers(limit = 50, offset = 0): Promise { const service = new UserService(); return await service.getAllUsers(limit, offset); } } // ===================================================== // 評審服務 // ===================================================== export class JudgeService { // 創建評審 static async createJudge(judgeData: Omit): Promise { 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 { const sql = 'SELECT * FROM judges WHERE name = ? AND is_active = TRUE'; const result = await db.queryOne(sql, [name]); if (result) { result.expertise = JSON.parse(result.expertise as any); } return result; } // 根據ID獲取評審 static async getJudgeById(id: string): Promise { const sql = 'SELECT * FROM judges WHERE id = ? AND is_active = TRUE'; const result = await db.queryOne(sql, [id]); if (result) { result.expertise = JSON.parse(result.expertise as any); } return result; } // 獲取所有評審 static async getAllJudges(): Promise { const sql = 'SELECT * FROM judges WHERE is_active = TRUE ORDER BY created_at DESC'; const results = await db.query(sql); return results.map(judge => ({ ...judge, expertise: JSON.parse(judge.expertise as any) })); } // 更新評審 static async updateJudge(id: string, updates: Partial): Promise { 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): Promise { 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 { const sql = 'SELECT * FROM competitions WHERE name = ? AND is_active = TRUE'; return await db.queryOne(sql, [name]); } // 根據ID獲取競賽 static async getCompetitionById(id: string): Promise { const sql = 'SELECT * FROM competitions WHERE id = ? AND is_active = TRUE'; return await db.queryOne(sql, [id]); } // 獲取所有競賽 static async getAllCompetitions(): Promise { const sql = 'SELECT * FROM competitions WHERE is_active = TRUE ORDER BY year DESC, month DESC'; return await db.query(sql); } // 獲取競賽統計 static async getCompetitionStatistics(id: string): Promise { const sql = 'SELECT * FROM competition_statistics WHERE id = ?'; return await db.queryOne(sql, [id]); } // 更新競賽 static async updateCompetition(id: string, updates: Partial): Promise { 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 { // 創建應用 async createApp(appData: { name: string; description: string; creator_id: string; category: string; type: string; app_url?: string; icon?: string; icon_color?: string; }): Promise<{ success: boolean; app?: any; error?: string }> { try { const appId = crypto.randomUUID(); const sql = ` INSERT INTO apps (id, name, description, creator_id, category, type, app_url, icon, icon_color, likes_count, views_count, rating, is_active, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0, 0.00, TRUE, NOW(), NOW()) `; await this.query(sql, [ appId, appData.name, appData.description, appData.creator_id, appData.category, appData.type, appData.app_url || null, appData.icon || 'Bot', appData.icon_color || 'from-blue-500 to-purple-500' ]); // 獲取創建的應用 const createdApp = await this.getAppById(appId); return { success: true, app: createdApp }; } catch (error) { console.error('創建應用錯誤:', error); return { success: false, error: '創建應用時發生錯誤' }; } } // 根據 ID 獲取應用(僅已發布) async getAppById(appId: string): Promise { const sql = ` SELECT a.*, u.name as creator_name, u.department as creator_department FROM apps a LEFT JOIN users u ON a.creator_id = u.id WHERE a.id = ? AND a.is_active = TRUE `; return await this.queryOne(sql, [appId]); } // 根據 ID 獲取應用(任何狀態) async getAppByIdAnyStatus(appId: string): Promise { const sql = ` SELECT a.*, u.name as creator_name, u.department as creator_department FROM apps a LEFT JOIN users u ON a.creator_id = u.id WHERE a.id = ? `; return await this.queryOne(sql, [appId]); } // 根據名稱獲取應用 async getAppByName(name: string): Promise { const sql = ` SELECT a.*, u.name as creator_name, u.department as creator_department FROM apps a LEFT JOIN users u ON a.creator_id = u.id WHERE a.name = ? AND a.is_active = TRUE `; return await this.queryOne(sql, [name]); } // 獲取所有應用 async getAllApps(filters: { search?: string; category?: string; type?: string; status?: string; page?: number; limit?: number; } = {}): Promise<{ apps: any[]; total: number }> { try { const { search = '', category = 'all', type = 'all', status = 'all', page = 1, limit = 10 } = filters; // 構建查詢條件 let whereConditions: string[] = []; let params: any[] = []; // 根據狀態篩選 if (status && status !== 'all') { if (status === 'active') { whereConditions.push('a.is_active = TRUE'); } else if (status === 'inactive') { whereConditions.push('a.is_active = FALSE'); } // 如果 status 是 'all' 或其他值,則不添加狀態篩選 } if (search) { whereConditions.push('(a.name LIKE ? OR a.description LIKE ? OR u.name LIKE ?)'); params.push(`%${search}%`, `%${search}%`, `%${search}%`); } if (category && category !== 'all') { whereConditions.push('a.category = ?'); params.push(category); } if (type && type !== 'all') { whereConditions.push('a.type = ?'); params.push(type); } const whereClause = whereConditions.length > 0 ? `WHERE ${whereConditions.join(' AND ')}` : ''; // 獲取總數 const countSql = ` SELECT COUNT(DISTINCT a.id) as total FROM apps a LEFT JOIN users u ON a.creator_id = u.id LEFT JOIN user_ratings ur ON a.id = ur.app_id ${whereClause} `; const countResult = await this.queryOne(countSql, params); const total = countResult?.total || 0; // 獲取應用列表 const offset = (page - 1) * limit; const sql = ` SELECT a.*, u.name as creator_name, u.department as creator_department, u.email as creator_email, COALESCE(AVG(ur.rating), 0) as rating, COUNT(ur.id) as reviewCount FROM apps a LEFT JOIN users u ON a.creator_id = u.id LEFT JOIN user_ratings ur ON a.id = ur.app_id ${whereClause} GROUP BY a.id, u.name, u.department, u.email ORDER BY a.created_at DESC LIMIT ${limit} OFFSET ${offset} `; const apps = await this.query(sql, params); return { apps, total }; } catch (error) { console.error('獲取應用列表錯誤:', error); return { apps: [], total: 0 }; } } // 更新應用 async updateApp(appId: string, updates: { name?: string; description?: string; category?: string; type?: string; app_url?: string; icon?: string; icon_color?: string; }): Promise<{ success: boolean; app?: any; error?: string }> { try { const updateFields = []; const params = []; if (updates.name !== undefined) { updateFields.push('name = ?'); params.push(updates.name); } if (updates.description !== undefined) { updateFields.push('description = ?'); params.push(updates.description); } if (updates.category !== undefined) { updateFields.push('category = ?'); params.push(updates.category); } if (updates.type !== undefined) { updateFields.push('type = ?'); params.push(updates.type); } if (updates.app_url !== undefined) { updateFields.push('app_url = ?'); params.push(updates.app_url); } if (updates.icon !== undefined) { updateFields.push('icon = ?'); params.push(updates.icon); } if (updates.icon_color !== undefined) { updateFields.push('icon_color = ?'); params.push(updates.icon_color); } if (updateFields.length === 0) { return { success: false, error: '沒有要更新的欄位' }; } updateFields.push('updated_at = NOW()'); params.push(appId); const sql = ` UPDATE apps SET ${updateFields.join(', ')} WHERE id = ? AND is_active = TRUE `; await this.query(sql, params); // 獲取更新後的應用 const updatedApp = await this.getAppById(appId); return { success: true, app: updatedApp }; } catch (error) { console.error('更新應用錯誤:', error); return { success: false, error: '更新應用時發生錯誤' }; } } // 刪除應用(硬刪除) async deleteApp(appId: string): Promise<{ success: boolean; error?: string }> { try { // 先檢查應用是否存在(任何狀態) const app = await this.getAppByIdAnyStatus(appId); if (!app) { return { success: false, error: '應用不存在' }; } // 硬刪除應用(從資料庫中完全移除) const sql = 'DELETE FROM apps WHERE id = ?'; await this.query(sql, [appId]); return { success: true }; } catch (error) { console.error('刪除應用錯誤:', error); return { success: false, error: '刪除應用時發生錯誤' }; } } // 切換應用狀態 async toggleAppStatus(appId: string): Promise<{ success: boolean; app?: any; error?: string }> { try { // 先獲取當前狀態(任何狀態) const currentApp = await this.getAppByIdAnyStatus(appId); if (!currentApp) { return { success: false, error: '應用不存在' }; } const newStatus = currentApp.is_active ? false : true; const sql = 'UPDATE apps SET is_active = ?, updated_at = NOW() WHERE id = ?'; await this.query(sql, [newStatus, appId]); // 獲取更新後的應用(任何狀態) const updatedApp = await this.getAppByIdAnyStatus(appId); return { success: true, app: updatedApp }; } catch (error) { console.error('切換應用狀態錯誤:', error); return { success: false, error: '切換應用狀態時發生錯誤' }; } } // 獲取應用統計 async getAppStats(): Promise<{ totalApps: number; activeApps: number; inactiveApps: number; pendingApps: number; totalViews: number; totalLikes: number; newThisMonth: number; }> { try { const sql = ` SELECT COUNT(*) as total_apps, COUNT(CASE WHEN is_active = TRUE THEN 1 END) as active_apps, COUNT(CASE WHEN is_active = FALSE THEN 1 END) as inactive_apps, COALESCE(SUM(views_count), 0) as total_views, COALESCE(SUM(likes_count), 0) as total_likes, COUNT(CASE WHEN created_at >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) THEN 1 END) as new_this_month FROM apps `; const result = await this.queryOne(sql); return { totalApps: result?.total_apps || 0, activeApps: result?.active_apps || 0, inactiveApps: result?.inactive_apps || 0, pendingApps: 0, // 目前沒有 pending 狀態 totalViews: result?.total_views || 0, totalLikes: result?.total_likes || 0, newThisMonth: result?.new_this_month || 0 }; } catch (error) { console.error('獲取應用統計錯誤:', error); return { totalApps: 0, activeApps: 0, inactiveApps: 0, pendingApps: 0, totalViews: 0, totalLikes: 0, newThisMonth: 0 }; } } // 通用查詢方法 async query(sql: string, params: any[] = []): Promise { return await db.query(sql, params); } // 通用單一查詢方法 async queryOne(sql: string, params: any[] = []): Promise { return await db.queryOne(sql, params); } // 獲取應用評分統計 async getAppRatingStats(appId: string): Promise<{ averageRating: number; totalRatings: number; ratingDistribution: { rating: number; count: number }[]; }> { try { const sql = ` SELECT AVG(rating) as average_rating, COUNT(*) as total_ratings FROM user_ratings WHERE app_id = ? `; const result = await this.queryOne(sql, [appId]); const distributionSql = ` SELECT rating, COUNT(*) as count FROM user_ratings WHERE app_id = ? GROUP BY rating ORDER BY rating DESC `; const distribution = await this.query(distributionSql, [appId]); return { averageRating: result.average_rating ? parseFloat(result.average_rating) : 0, totalRatings: result.total_ratings || 0, ratingDistribution: distribution.map((row: any) => ({ rating: row.rating, count: row.count })) }; } catch (error) { console.error('獲取應用評分統計錯誤:', error); return { averageRating: 0, totalRatings: 0, ratingDistribution: [] }; } } // 獲取應用使用統計 async getAppUsageStats(appId: string): Promise<{ dailyUsers: number; weeklyUsers: number; monthlyUsers: number; totalSessions: number; topDepartments: { department: string; count: number }[]; trendData: { date: string; users: number }[]; }> { try { // 今日使用者 const dailySql = ` SELECT COUNT(DISTINCT user_id) as daily_users FROM user_views WHERE app_id = ? AND DATE(viewed_at) = CURDATE() `; const dailyResult = await this.queryOne(dailySql, [appId]); // 本週使用者 const weeklySql = ` SELECT COUNT(DISTINCT user_id) as weekly_users FROM user_views WHERE app_id = ? AND viewed_at >= DATE_SUB(CURDATE(), INTERVAL 1 WEEK) `; const weeklyResult = await this.queryOne(weeklySql, [appId]); // 本月使用者 const monthlySql = ` SELECT COUNT(DISTINCT user_id) as monthly_users FROM user_views WHERE app_id = ? AND viewed_at >= DATE_SUB(CURDATE(), INTERVAL 1 MONTH) `; const monthlyResult = await this.queryOne(monthlySql, [appId]); // 總使用次數 const totalSql = ` SELECT COUNT(*) as total_sessions FROM user_views WHERE app_id = ? `; const totalResult = await this.queryOne(totalSql, [appId]); // 部門使用統計 const deptSql = ` SELECT u.department, COUNT(*) as count FROM user_views uv JOIN users u ON uv.user_id = u.id WHERE uv.app_id = ? GROUP BY u.department ORDER BY count DESC LIMIT 5 `; const deptResult = await this.query(deptSql, [appId]); // 使用趨勢(過去7天) const trendSql = ` SELECT DATE(viewed_at) as date, COUNT(DISTINCT user_id) as users FROM user_views WHERE app_id = ? AND viewed_at >= DATE_SUB(CURDATE(), INTERVAL 7 DAY) GROUP BY DATE(viewed_at) ORDER BY date ASC `; const trendResult = await this.query(trendSql, [appId]); return { dailyUsers: dailyResult.daily_users || 0, weeklyUsers: weeklyResult.weekly_users || 0, monthlyUsers: monthlyResult.monthly_users || 0, totalSessions: totalResult.total_sessions || 0, topDepartments: deptResult.map((row: any) => ({ department: row.department, count: row.count })), trendData: trendResult.map((row: any) => ({ date: row.date, users: row.users })) }; } catch (error) { console.error('獲取應用使用統計錯誤:', error); return { dailyUsers: 0, weeklyUsers: 0, monthlyUsers: 0, totalSessions: 0, topDepartments: [], trendData: [] }; } } // 獲取應用評價列表 async getAppReviews(appId: string, limit: number = 10, offset: number = 0): Promise { try { const sql = ` SELECT ur.id, ur.rating, ur.rated_at, u.name as user_name, u.department as user_department, u.avatar as user_avatar FROM user_ratings ur JOIN users u ON ur.user_id = u.id WHERE ur.app_id = ? ORDER BY ur.rated_at DESC LIMIT ${limit} OFFSET ${offset} `; const reviews = await this.query(sql, [appId]); return reviews.map((review: any) => ({ id: review.id, rating: review.rating, review: '用戶評價', // 暫時使用固定文字,因為資料庫中沒有 review 欄位 ratedAt: review.rated_at, userName: review.user_name, userDepartment: review.user_department, userAvatar: review.user_avatar })); } catch (error) { console.error('獲取應用評價列表錯誤:', error); return []; } } // 獲取應用評價總數 async getAppReviewCount(appId: string): Promise { try { const sql = ` SELECT COUNT(*) as count FROM user_ratings WHERE app_id = ? `; const result = await this.queryOne(sql, [appId]); return result.count || 0; } catch (error) { console.error('獲取應用評價總數錯誤:', error); return 0; } } // 刪除評價 async deleteReview(reviewId: string, appId: string): Promise<{ success: boolean; error?: string }> { try { // 檢查評價是否存在且屬於該應用 const checkSql = ` SELECT id FROM user_ratings WHERE id = ? AND app_id = ? `; const review = await this.queryOne(checkSql, [reviewId, appId]); if (!review) { return { success: false, error: '評價不存在或無權限刪除' }; } // 刪除評價 const deleteSql = 'DELETE FROM user_ratings WHERE id = ?'; await this.query(deleteSql, [reviewId]); return { success: true }; } catch (error) { console.error('刪除評價錯誤:', error); return { success: false, error: '刪除評價時發生錯誤' }; } } } // ===================================================== // 評分服務 // ===================================================== export class ScoringService { // 提交應用評分 static async submitAppScore(scoreData: Omit): Promise { 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 { const sql = 'SELECT * FROM app_judge_scores WHERE judge_id = ? AND app_id = ?'; return await db.queryOne(sql, [judgeId, appId]); } // 獲取應用的所有評分 static async getAppScores(appId: string): Promise { const sql = 'SELECT * FROM app_judge_scores WHERE app_id = ? ORDER BY submitted_at DESC'; return await db.query(sql, [appId]); } // 提交提案評分 static async submitProposalScore(scoreData: Omit): Promise { 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 { const sql = 'SELECT * FROM proposal_judge_scores WHERE judge_id = ? AND proposal_id = ?'; return await db.queryOne(sql, [judgeId, proposalId]); } // 獲取提案的所有評分 static async getProposalScores(proposalId: string): Promise { const sql = 'SELECT * FROM proposal_judge_scores WHERE proposal_id = ? ORDER BY submitted_at DESC'; return await db.query(sql, [proposalId]); } } // ===================================================== // 獎項服務 // ===================================================== export class AwardService { // 創建獎項 static async createAward(awardData: Omit): Promise { 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 { const sql = 'SELECT * FROM awards WHERE competition_id = ? AND creator = ? ORDER BY created_at DESC LIMIT 1'; return await db.queryOne(sql, [competitionId, creator]); } // 根據年份獲取獎項 static async getAwardsByYear(year: number): Promise { const sql = 'SELECT * FROM awards WHERE year = ? ORDER BY month DESC, rank ASC'; return await db.query(sql, [year]); } // 根據競賽獲取獎項 static async getAwardsByCompetition(competitionId: string): Promise { const sql = 'SELECT * FROM awards WHERE competition_id = ? ORDER BY rank ASC'; return await db.query(sql, [competitionId]); } } // ===================================================== // 系統設定服務 // ===================================================== export class SystemSettingService { // 獲取設定值 static async getSetting(key: string): Promise { 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 { 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 { const sql = 'SELECT * FROM system_settings WHERE is_public = TRUE ORDER BY category, `key`'; return await db.query(sql); } }