// ===================================================== // 資料庫服務層 // ===================================================== import { db } from '../database'; import { dbSync } from '../database-sync'; import { DatabaseServiceBase } from '../database-service-base'; const { DatabaseSyncFixed } = require('../database-sync-fixed.js'); 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 extends DatabaseServiceBase { // 創建用戶 async create(userData: Omit & { id: string }): 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 DatabaseServiceBase.safeInsert(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 this.safeQueryOne(sql, [email]); } // 根據ID獲取用戶 async findById(id: string): Promise { const sql = 'SELECT * FROM users WHERE id = ? AND status = "active"'; return await this.safeQueryOne(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 DatabaseServiceBase.safeUpdate(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 DatabaseServiceBase.safeUpdate(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 = 5 } = 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 DatabaseServiceBase.safeQuery(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.safeQuery(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 DatabaseServiceBase.safeQuery(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 = 1 `; 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 = 1 `; 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_APP_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 this.safeQuery(sql, params); } // 獲取儀表板統計數據 async getDashboardStats(): Promise<{ totalUsers: number; activeUsers: number; totalApps: number; activeApps: number; inactiveApps: number; pendingApps: 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 is_active = 1 THEN 1 END) as active_apps, COUNT(CASE WHEN is_active = FALSE THEN 1 END) as inactive_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 `; const appStats = await this.queryOne(appStatsSql); // 評論統計 const reviewStatsSql = ` SELECT COUNT(*) as total_reviews FROM user_ratings `; const reviewStats = await this.queryOne(reviewStatsSql); // 競賽統計 - 使用動態計算的狀態 const competitions = await CompetitionService.getAllCompetitions(); // 動態計算每個競賽的狀態 const now = new Date(); const competitionsWithCalculatedStatus = competitions.map(competition => { const startDate = new Date(competition.start_date); const endDate = new Date(competition.end_date); let calculatedStatus = competition.status; // 確保日期比較的準確性,使用 UTC 時間避免時區問題 const nowUTC = new Date(now.getTime() + now.getTimezoneOffset() * 60000); const startDateUTC = new Date(startDate.getTime() + startDate.getTimezoneOffset() * 60000); const endDateUTC = new Date(endDate.getTime() + endDate.getTimezoneOffset() * 60000); // 根據實際日期計算狀態 if (nowUTC < startDateUTC) { calculatedStatus = 'upcoming'; // 即將開始 } else if (nowUTC >= startDateUTC && nowUTC <= endDateUTC) { calculatedStatus = 'active'; // 進行中 } else if (nowUTC > endDateUTC) { calculatedStatus = 'completed'; // 已完成 } return { ...competition, status: calculatedStatus }; }); const competitionStats = { total_competitions: competitionsWithCalculatedStatus.length, active_competitions: competitionsWithCalculatedStatus.filter(c => c.status === 'active').length }; // 計算增長率(與上個月比較) 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, activeApps: appStats?.active_apps || 0, inactiveApps: appStats?.inactive_apps || 0, pendingApps: 0, // 目前沒有pending狀態的應用 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, activeApps: 0, inactiveApps: 0, pendingApps: 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 = 1 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 this.safeQueryOne(sql, params); } // 獲取所有用戶 async getAllUsers(limit = 50, offset = 0): Promise { const sql = 'SELECT * FROM users ORDER BY created_at DESC LIMIT ? OFFSET ?'; return await this.safeQuery(sql, [limit, offset]); } // 靜態方法保持向後兼容 static async createUser(userData: Omit & { id?: string }): Promise { const service = new UserService(); // 確保 id 存在 if (!userData.id) { userData.id = `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } return await service.create(userData as Omit & { id: string }); } 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); } // 獲取用戶的應用和評價統計 async getUserAppAndReviewStats(userId: string): Promise<{ appCount: number; reviewCount: number; }> { try { // 獲取用戶創建的應用數量 const appCountSql = ` SELECT COUNT(*) as app_count FROM apps WHERE creator_id = ? `; // 獲取用戶撰寫的評價數量 const reviewCountSql = ` SELECT COUNT(*) as review_count FROM user_ratings WHERE user_id = ? `; const [appResult, reviewResult] = await Promise.all([ this.queryOne(appCountSql, [userId]), this.queryOne(reviewCountSql, [userId]) ]); const result = { appCount: appResult?.app_count || 0, reviewCount: reviewResult?.review_count || 0 }; return result; } catch (error) { console.error('獲取用戶應用和評價統計錯誤:', error); return { appCount: 0, reviewCount: 0 }; } } } // ===================================================== // 評審服務 // ===================================================== export class JudgeService extends DatabaseServiceBase { // 安全解析 expertise 字段 private static parseExpertise(expertise: any): string[] { if (!expertise) return []; // 如果已經是數組,直接返回 if (Array.isArray(expertise)) return expertise; // 如果是字符串 if (typeof expertise === 'string') { // 嘗試解析為 JSON try { const parsed = JSON.parse(expertise); if (Array.isArray(parsed)) return parsed; } catch (e) { // 如果 JSON 解析失敗,嘗試按逗號分割 return expertise.split(',').map(item => item.trim()).filter(item => item); } } return []; } // 創建評審 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 DatabaseServiceBase.safeInsert(sql, params); return await JudgeService.getJudgeByName(judgeData.name) as Judge; } // 根據姓名獲取評審 static async getJudgeByName(name: string): Promise { const sql = 'SELECT * FROM judges WHERE name = ?'; const result = await db.queryOne(sql, [name]); if (result) { result.expertise = this.parseExpertise(result.expertise as any); } return result; } // 根據ID獲取評審 static async getJudgeById(id: string): Promise { const sql = 'SELECT * FROM judges WHERE id = ?'; const result = await db.queryOne(sql, [id]); if (result) { result.expertise = this.parseExpertise(result.expertise as any); result.is_active = Boolean(result.is_active); // 確保 is_active 是布爾值 } return result; } // 獲取所有評審 static async getAllJudges(): Promise { const sql = 'SELECT * FROM judges ORDER BY created_at DESC'; const results = await db.query(sql); return results.map(judge => ({ ...judge, expertise: this.parseExpertise(judge.expertise as any), is_active: Boolean(judge.is_active) // 確保 is_active 是布爾值 })); } // 更新評審 static async updateJudge(id: string, updates: Partial): Promise { const fields = Object.keys(updates).filter(key => key !== 'id' && key !== 'created_at'); if (fields.length === 0) { return true; // 沒有需要更新的字段,視為成功 } 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 DatabaseServiceBase.safeUpdate(sql, [...values, id]); return result.affectedRows > 0; } // 刪除評審(硬刪除) static async deleteJudge(id: string): Promise { try { const sql = 'DELETE FROM judges WHERE id = ?'; const result = await DatabaseServiceBase.safeDelete(sql, [id]); return result.affectedRows > 0; } catch (error) { console.error('刪除評審錯誤:', error); return false; } } // 獲取評審的評分任務 static async getJudgeScoringTasks(judgeId: string, competitionId?: string): Promise { console.log('🔍 JudgeService.getJudgeScoringTasks 被調用'); console.log('judgeId:', judgeId); console.log('competitionId:', competitionId); let sql: string; let params: any[]; if (competitionId) { // 獲取特定競賽的評分任務 // 使用 UNION 來合併個人競賽和團隊競賽的應用程式 sql = ` SELECT DISTINCT a.id, a.name, 'app' as type, 'individual' as participant_type, COALESCE(js.total_score, 0) as score, CASE WHEN js.total_score > 0 THEN 'completed' ELSE 'pending' END as status, js.submitted_at, NULL as team_name, a.name as display_name FROM apps a INNER JOIN competition_apps ca ON a.id = ca.app_id LEFT JOIN judge_scores js ON a.id = js.app_id AND js.judge_id = ? AND js.competition_id = ? WHERE ca.competition_id = ? AND a.is_active = 1 UNION ALL SELECT DISTINCT a.id, a.name, 'app' as type, 'team' as participant_type, COALESCE(js.total_score, 0) as score, CASE WHEN js.total_score > 0 THEN 'completed' ELSE 'pending' END as status, js.submitted_at, t.name as team_name, CONCAT(t.name, ' - ', a.name) as display_name FROM apps a INNER JOIN competition_teams ct ON a.team_id = ct.team_id LEFT JOIN teams t ON a.team_id = t.id LEFT JOIN judge_scores js ON a.id = js.app_id AND js.judge_id = ? AND js.competition_id = ? WHERE ct.competition_id = ? AND a.is_active = 1 ORDER BY display_name `; params = [judgeId, competitionId, competitionId, judgeId, competitionId, competitionId]; } else { // 獲取所有競賽的任務 sql = ` SELECT DISTINCT a.id, a.name, 'app' as type, 'individual' as participant_type, COALESCE(js.total_score, 0) as score, CASE WHEN js.total_score > 0 THEN 'completed' ELSE 'pending' END as status, js.submitted_at, NULL as team_name, a.name as display_name FROM apps a INNER JOIN competition_apps ca ON a.id = ca.app_id LEFT JOIN judge_scores js ON a.id = js.app_id AND js.judge_id = ? WHERE a.is_active = 1 UNION ALL SELECT DISTINCT a.id, a.name, 'app' as type, 'team' as participant_type, COALESCE(js.total_score, 0) as score, CASE WHEN js.total_score > 0 THEN 'completed' ELSE 'pending' END as status, js.submitted_at, t.name as team_name, CONCAT(t.name, ' - ', a.name) as display_name FROM apps a INNER JOIN competition_teams ct ON a.team_id = ct.team_id LEFT JOIN teams t ON a.team_id = t.id LEFT JOIN judge_scores js ON a.id = js.app_id AND js.judge_id = ? WHERE a.is_active = 1 ORDER BY display_name `; params = [judgeId, judgeId]; } console.log('🔍 執行SQL查詢:'); console.log('SQL:', sql); console.log('參數:', params); const results = await DatabaseServiceBase.safeQuery(sql, params); console.log('📊 查詢結果:', results); return results; } } // ===================================================== // 團隊服務 // ===================================================== export class TeamService extends DatabaseServiceBase { // 創建團隊 static async createTeam(teamData: { name: string; leader_id: string; department: string; contact_email: string; description?: string; }): Promise { const id = `t${Date.now()}${Math.random().toString(36).substr(2, 9)}`; const sql = ` INSERT INTO teams (id, name, leader_id, department, contact_email, description) VALUES (?, ?, ?, ?, ?, ?) `; const params = [ id, teamData.name, teamData.leader_id, teamData.department, teamData.contact_email, teamData.description || null ]; const result = await DatabaseServiceBase.safeInsert(sql, params); return id; } // 獲取所有團隊 static async getAllTeams(): Promise { const sql = ` SELECT t.*, u.name as leader_name, u.phone as leader_phone, t.leader_id as leader, COUNT(DISTINCT tm.id) as member_count, COUNT(DISTINCT a.id) as app_count, t.created_at as submissionDate FROM teams t LEFT JOIN users u ON t.leader_id = u.id LEFT JOIN team_members tm ON t.id = tm.team_id LEFT JOIN apps a ON t.id = a.team_id AND a.is_active = 1 WHERE t.is_active = 1 GROUP BY t.id, t.name, t.leader_id, t.department, t.contact_email, t.total_likes, t.is_active, t.created_at, t.updated_at, u.name, u.phone ORDER BY t.created_at DESC `; const results = await DatabaseServiceBase.safeQuery(sql); return results; } // 根據 ID 獲取團隊 static async getTeamById(id: string): Promise { const sql = ` SELECT t.*, u.name as leader_name, u.phone as leader_phone FROM teams t LEFT JOIN users u ON t.leader_id = u.id WHERE t.id = ? AND t.is_active = 1 `; const results = await DatabaseServiceBase.safeQuery(sql, [id]); return results.length > 0 ? results[0] : null; } // 根據名稱獲取團隊 static async getTeamByName(name: string): Promise { const sql = ` SELECT t.*, u.name as leader_name, u.phone as leader_phone FROM teams t LEFT JOIN users u ON t.leader_id = u.id WHERE t.name = ? AND t.is_active = 1 `; const results = await DatabaseServiceBase.safeQuery(sql, [name]); return results.length > 0 ? results[0] : null; } // 更新團隊 static async updateTeam(id: string, updates: Partial<{ name: string; leader_id: string; department: string; contact_email: string; description: string; total_likes: number; members?: Array<{ user_id: string; role: string; }>; apps?: string[]; }>): Promise { try { // 更新團隊基本信息 const teamFields = Object.keys(updates).filter(key => key !== 'id' && key !== 'created_at' && key !== 'members' && key !== 'apps' ); if (teamFields.length > 0) { const setClause = teamFields.map(field => `${field} = ?`).join(', '); const values = teamFields.map(field => updates[field as keyof typeof updates]); values.push(id); const sql = `UPDATE teams SET ${setClause}, updated_at = CURRENT_TIMESTAMP WHERE id = ?`; const result = await DatabaseServiceBase.safeUpdate(sql, values); console.log('團隊基本信息更新結果:', result); if (result.affectedRows === 0) { console.log('團隊基本信息更新失敗'); return false; } } // 更新團隊成員(如果明確提供了成員信息) if (updates.members !== undefined && Array.isArray(updates.members)) { try { // 先刪除現有成員 console.log('刪除現有團隊成員...'); await DatabaseServiceBase.safeDelete('DELETE FROM team_members WHERE team_id = ?', [id]); // 添加新成員(如果成員列表不為空) if (updates.members.length > 0) { console.log('添加新團隊成員...'); for (const member of updates.members) { if (member.user_id && member.role) { console.log(`添加成員: ${member.user_id} (${member.role})`); // 生成唯一的 ID const memberId = `tm_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; await DatabaseServiceBase.safeInsert( 'INSERT INTO team_members (id, team_id, user_id, role, joined_at) VALUES (?, ?, ?, ?, NOW())', [memberId, id, member.user_id, member.role] ); } } console.log('團隊成員更新完成'); } else { console.log('成員列表為空,只刪除現有成員'); } } catch (memberError) { console.error('更新團隊成員時發生錯誤:', memberError); // 成員更新失敗不應該影響整個更新操作 // 可以選擇繼續或回滾 } } else { console.log('未提供成員信息,跳過成員更新'); } // 更新團隊應用(如果明確提供了應用信息) if (updates.apps !== undefined && Array.isArray(updates.apps)) { try { // 先移除現有應用的團隊關聯 console.log('移除現有應用的團隊關聯...'); await DatabaseServiceBase.safeUpdate('UPDATE apps SET team_id = NULL WHERE team_id = ?', [id]); // 添加新應用的團隊關聯(如果應用列表不為空) if (updates.apps.length > 0) { console.log('添加新應用的團隊關聯...'); for (const appId of updates.apps) { if (appId) { console.log(`關聯應用: ${appId}`); await DatabaseServiceBase.safeUpdate( 'UPDATE apps SET team_id = ? WHERE id = ?', [id, appId] ); } } console.log('團隊應用更新完成'); } else { console.log('應用列表為空,只移除現有關聯'); } } catch (appError) { console.error('更新團隊應用時發生錯誤:', appError); // 應用更新失敗不應該影響整個更新操作 } } else { console.log('未提供應用信息,跳過應用更新'); } return true; } catch (error) { console.error('更新團隊錯誤:', error); return false; } } // 刪除團隊(軟刪除) static async deleteTeam(id: string): Promise { try { const sql = 'UPDATE teams SET is_active = FALSE, updated_at = CURRENT_TIMESTAMP WHERE id = ?'; const result = await DatabaseServiceBase.safeUpdate(sql, [id]); return result.affectedRows > 0; } catch (error) { console.error('刪除團隊錯誤:', error); return false; } } // 硬刪除團隊 static async hardDeleteTeam(id: string): Promise { try { const sql = 'DELETE FROM teams WHERE id = ?'; const result = await DatabaseServiceBase.safeDelete(sql, [id]); return result.affectedRows > 0; } catch (error) { console.error('硬刪除團隊錯誤:', error); return false; } } // 添加團隊成員 static async addTeamMember(teamId: string, userId: string, role: string = 'member'): Promise { const id = `tm${Date.now()}${Math.random().toString(36).substr(2, 9)}`; const sql = ` INSERT INTO team_members (id, team_id, user_id, role) VALUES (?, ?, ?, ?) `; const params = [id, teamId, userId, role]; try { const result = await DatabaseServiceBase.safeInsert(sql, params); console.log('團隊成員添加結果:', result); return result.affectedRows > 0; } catch (error) { console.error('添加團隊成員錯誤:', error); return false; } } // 獲取團隊成員 static async getTeamMembers(teamId: string): Promise { const sql = ` SELECT tm.*, u.name, u.department, u.email FROM team_members tm LEFT JOIN users u ON tm.user_id = u.id WHERE tm.team_id = ? AND u.status = 'active' ORDER BY tm.joined_at ASC `; const results = await DatabaseServiceBase.safeQuery(sql, [teamId]); return results; } // 移除團隊成員 static async removeTeamMember(teamId: string, userId: string): Promise { try { const sql = 'DELETE FROM team_members WHERE team_id = ? AND user_id = ?'; const result = await DatabaseServiceBase.safeDelete(sql, [teamId, userId]); return result.affectedRows > 0; } catch (error) { console.error('移除團隊成員錯誤:', error); return false; } } // 更新團隊成員角色 static async updateTeamMemberRole(teamId: string, userId: string, role: string): Promise { try { const sql = 'UPDATE team_members SET role = ? WHERE team_id = ? AND user_id = ?'; const result = await DatabaseServiceBase.safeUpdate(sql, [role, teamId, userId]); return result.affectedRows > 0; } catch (error) { console.error('更新團隊成員角色錯誤:', error); return false; } } // 綁定應用到團隊 static async bindAppToTeam(teamId: string, appId: string): Promise { try { const sql = 'UPDATE apps SET team_id = ? WHERE id = ? AND is_active = 1'; const result = await DatabaseServiceBase.safeUpdate(sql, [teamId, appId]); return result.affectedRows > 0; } catch (error) { console.error('綁定應用到團隊錯誤:', error); return false; } } // 解除應用與團隊的綁定 static async unbindAppFromTeam(appId: string): Promise { try { const sql = 'UPDATE apps SET team_id = NULL WHERE id = ?'; const result = await DatabaseServiceBase.safeUpdate(sql, [appId]); return result.affectedRows > 0; } catch (error) { console.error('解除應用與團隊綁定錯誤:', error); return false; } } // 獲取團隊的應用列表 static async getTeamApps(teamId: string): Promise { console.log('🔍 TeamService.getTeamApps 被調用, teamId:', teamId); const sql = ` SELECT a.id, a.name, a.description, a.category, a.type, a.icon, a.icon_color, a.app_url, a.likes_count, a.views_count, a.rating, a.created_at, 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.team_id = ? AND a.is_active = 1 ORDER BY a.created_at DESC `; const results = await DatabaseServiceBase.safeQuery(sql, [teamId]); return results; } // 獲取團隊統計 static async getTeamStats(): Promise { const sql = ` SELECT COUNT(*) as totalTeams, COUNT(CASE WHEN is_active = 1 THEN 1 END) as activeTeams, COUNT(CASE WHEN is_active = FALSE THEN 1 END) as inactiveTeams, AVG(member_count) as avgMembersPerTeam FROM ( SELECT t.id, t.is_active, COUNT(tm.id) as member_count FROM teams t LEFT JOIN team_members tm ON t.id = tm.team_id GROUP BY t.id ) as team_stats `; const results = await DatabaseServiceBase.safeQuery(sql); return results[0] || { totalTeams: 0, activeTeams: 0, inactiveTeams: 0, avgMembersPerTeam: 0 }; } } // ===================================================== // 競賽服務 // ===================================================== export class CompetitionService extends DatabaseServiceBase { // 創建競賽 static async createCompetition(competitionData: Omit): Promise { // 使用智能雙寫,每個資料庫生成自己的 ID const data = { name: competitionData.name, year: competitionData.year, month: competitionData.month, start_date: competitionData.start_date, end_date: competitionData.end_date, status: competitionData.status, description: competitionData.description || null, type: competitionData.type, evaluation_focus: competitionData.evaluation_focus || null, max_team_size: competitionData.max_team_size || null, is_active: competitionData.is_active, created_at: new Date(), updated_at: new Date() }; const dbSyncFixed = new DatabaseSyncFixed(); const result = await dbSyncFixed.smartDualInsert('competitions', data); if (!result.success) { throw new Error(`競賽創建失敗: 主機${result.masterError || '成功'}, 備機${result.slaveError || '成功'}`); } // 返回主機的競賽記錄(如果主機成功) if (result.masterSuccess) { const competition = await this.getCompetitionById(result.masterId!) as Competition; // 添加備機 ID 到競賽對象中,用於關聯表寫入 (competition as any).slaveId = result.slaveId; return competition; } else { // 如果主機失敗但備機成功,從備機獲取 const competition = await this.getCompetitionById(result.slaveId!) as Competition; (competition as any).slaveId = result.slaveId; return competition; } } // 根據 ID 獲取競賽 static async getCompetitionById(id: string): Promise { const sql = 'SELECT * FROM competitions WHERE id = ?'; return await db.queryOne(sql, [id]); } // 根據名稱獲取競賽 static async getCompetitionByName(name: string): Promise { const sql = 'SELECT * FROM competitions WHERE name = ? AND is_active = 1'; return await db.queryOne(sql, [name]); } // 根據名稱獲取備機競賽 ID static async getSlaveCompetitionIdByName(name: string): Promise { try { const dbSyncFixed = new DatabaseSyncFixed(); const slavePool = (dbSyncFixed as any).slavePool; if (!slavePool) return null; const connection = await slavePool.getConnection(); try { const [rows] = await connection.execute('SELECT id FROM competitions WHERE name = ? ORDER BY created_at DESC LIMIT 1', [name]); const result = (rows as any[])[0]?.id || null; // 確保返回的是字符串 if (result && typeof result !== 'string') { return String(result); } return result; } finally { connection.release(); } } catch (error) { console.error('獲取備機競賽 ID 失敗:', error); return null; } } // 獲取所有競賽 static async getAllCompetitions(): Promise { const sql = 'SELECT * FROM competitions WHERE is_active = 1 ORDER BY year DESC, month DESC'; const competitions = await db.query(sql); // 轉換字段名稱以匹配前端期望的格式 return competitions.map(competition => ({ ...competition, startDate: competition.start_date, endDate: competition.end_date, evaluationFocus: competition.evaluation_focus, maxTeamSize: competition.max_team_size, isActive: competition.is_active, createdAt: competition.created_at, updatedAt: competition.updated_at, // 添加默認的空數組,因為這些字段在競賽列表中不需要詳細數據 judges: [], participatingApps: [], participatingTeams: [], participatingProposals: [], rules: [], awardTypes: [] })); } // 獲取競賽統計 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 { try { const dbSyncFixed = new DatabaseSyncFixed(); // 使用雙寫功能更新競賽 const result = await dbSyncFixed.smartDualUpdate('competitions', id, updates); if (!result.success) { console.error('競賽更新失敗:', result.masterError || result.slaveError); return false; } return true; } catch (error) { console.error('競賽更新失敗:', error); return false; } } // 獲取當前競賽 static async getCurrentCompetition(): Promise { try { const sql = 'SELECT * FROM competitions WHERE is_current = 1 AND is_active = 1 LIMIT 1'; const competitions = await db.query(sql); if (competitions.length > 0) { const competition = competitions[0]; return await this.getCompetitionWithDetails(competition.id); } return null; } catch (error) { console.error('獲取當前競賽失敗:', error); return null; } } // 設置當前競賽 static async setCurrentCompetition(competitionId: string): Promise { try { const dbSyncFixed = new DatabaseSyncFixed(); // 先清除所有競賽的當前狀態 - 使用直接 SQL 更新 try { await dbSyncFixed.clearAllCurrentCompetitions(); } catch (error) { console.error('清除當前競賽狀態失敗:', error); } // 設置指定競賽為當前競賽 const setResult = await dbSyncFixed.smartDualUpdate('competitions', competitionId, { is_current: true }); if (!setResult.success) { console.error('設置當前競賽失敗:', setResult.masterError || setResult.slaveError); return false; } return true; } catch (error) { console.error('設置當前競賽失敗:', error); return false; } } // 清除當前競賽 static async clearCurrentCompetition(): Promise { try { await DatabaseServiceBase.safeUpdate('UPDATE competitions SET is_current = FALSE'); return true; } catch (error) { console.error('清除當前競賽失敗:', error); return false; } } // 刪除競賽 - 支援雙寫 static async deleteCompetition(id: string): Promise { try { const dbSyncFixed = new DatabaseSyncFixed(); // 先獲取競賽信息 const competition = await this.getCompetitionById(id); if (!competition) { console.error('競賽不存在'); return false; } // 刪除關聯數據 await this.deleteCompetitionRelations(id); // 使用雙寫功能刪除競賽 const result = await dbSyncFixed.smartDualDelete('competitions', id); if (!result.success) { console.error('競賽刪除失敗:', result.masterError || result.slaveError); return false; } return true; } catch (error) { console.error('競賽刪除失敗:', error); return false; } } // 刪除競賽關聯數據 private static async deleteCompetitionRelations(competitionId: string): Promise { try { const dbSyncFixed = new DatabaseSyncFixed(); // 刪除所有關聯表數據 const relations = [ 'competition_judges', 'competition_teams', 'competition_apps', 'competition_award_types', 'competition_rules' ]; for (const relationTable of relations) { try { await dbSyncFixed.smartDualDelete(relationTable, competitionId, 'competition_id'); } catch (error) { console.error(`刪除關聯表 ${relationTable} 失敗:`, error); } } } catch (error) { console.error('刪除競賽關聯數據失敗:', error); } } // ===================================================== // 競賽關聯數據管理方法 // ===================================================== // 為競賽添加評審 static async addCompetitionJudges(competitionId: string, judgeIds: string[]): Promise { try { const dbSyncFixed = new DatabaseSyncFixed(); // 先刪除現有的評審關聯 await DatabaseServiceBase.safeDelete('DELETE FROM competition_judges WHERE competition_id = ?', [competitionId]); // 添加新的評審關聯 if (judgeIds.length > 0) { // 獲取備機的競賽 ID - 通過名稱查找 const competition = await this.getCompetitionById(competitionId); const slaveCompetitionId = await this.getSlaveCompetitionIdByName(competition?.name || ''); if (!slaveCompetitionId) { console.error('找不到備機競賽 ID'); return false; } const relationData = judgeIds.map(judgeId => ({ judge_id: judgeId })); const result = await dbSyncFixed.smartDualInsertRelation( 'competition_judges', competitionId, slaveCompetitionId, relationData, 'judge_id' ); if (!result.success) { console.error('添加競賽評審失敗:', result.masterError || result.slaveError); return false; } } return true; } catch (error) { console.error('添加競賽評審失敗:', error); return false; } } // 從競賽中移除評審 static async removeCompetitionJudge(competitionId: string, judgeId: string): Promise { const sql = 'DELETE FROM competition_judges WHERE competition_id = ? AND judge_id = ?'; const result = await DatabaseServiceBase.safeDelete(sql, [competitionId, judgeId]); return result.affectedRows > 0; } // 獲取競賽的團隊列表 static async getCompetitionTeams(competitionId: string): Promise { const sql = ` SELECT t.*, ct.registered_at, u.name as leader_name, u.phone as leader_phone, COUNT(DISTINCT tm.id) as actual_member_count, COUNT(DISTINCT a.id) as app_count FROM competition_teams ct JOIN teams t ON ct.team_id = t.id LEFT JOIN users u ON t.leader_id = u.id LEFT JOIN team_members tm ON t.id = tm.team_id LEFT JOIN apps a ON t.id = a.team_id AND a.is_active = 1 WHERE ct.competition_id = ? AND t.is_active = 1 GROUP BY t.id, t.name, t.leader_id, t.department, t.contact_email, t.total_likes, t.is_active, t.created_at, t.updated_at, ct.registered_at, u.name, u.phone ORDER BY ct.registered_at ASC `; return await DatabaseServiceBase.safeQuery(sql, [competitionId]); } // 獲取競賽的應用列表 static async getCompetitionApps(competitionId: string, competitionType?: string): Promise { // 先獲取競賽信息 const competition = await this.getCompetitionById(competitionId); if (!competition) return []; let apps: any[] = []; if (competition.type === 'team') { // 對於團隊競賽,獲取所有參賽團隊的應用 const teams = await this.getCompetitionTeams(competitionId); if (teams.length > 0) { // 過濾掉 undefined 或 null 的 team_id 值 const teamIds = teams .map(t => t.id) .filter(id => id !== undefined && id !== null); if (teamIds.length > 0) { const placeholders = teamIds.map(() => '?').join(','); const sql = ` SELECT a.*, u.name as creator_name, u.department as creator_department, t.name as team_name FROM apps a LEFT JOIN users u ON a.creator_id = u.id LEFT JOIN teams t ON a.team_id = t.id WHERE a.team_id IN (${placeholders}) AND a.is_active = 1 ORDER BY a.created_at ASC `; apps = await DatabaseServiceBase.safeQuery(sql, teamIds); } } } else { // 對於個人競賽,從 competition_apps 表獲取 const sql = ` SELECT a.*, ca.submitted_at, u.name as creator_name, u.department as creator_department FROM competition_apps ca JOIN apps a ON ca.app_id = a.id LEFT JOIN users u ON a.creator_id = u.id WHERE ca.competition_id = ? AND a.is_active = 1 ORDER BY ca.submitted_at ASC `; apps = await DatabaseServiceBase.safeQuery(sql, [competitionId]); } return apps; } // 為競賽添加團隊 static async addCompetitionTeams(competitionId: string, teamIds: string[]): Promise { try { const dbSyncFixed = new DatabaseSyncFixed(); // 先刪除現有的團隊關聯 await DatabaseServiceBase.safeDelete('DELETE FROM competition_teams WHERE competition_id = ?', [competitionId]); // 添加新的團隊關聯 if (teamIds.length > 0) { // 獲取備機的競賽 ID - 通過名稱查找 const competition = await this.getCompetitionById(competitionId); const slaveCompetitionId = await this.getSlaveCompetitionIdByName(competition?.name || ''); if (!slaveCompetitionId) { console.error('找不到備機競賽 ID'); return false; } const relationData = teamIds.map(teamId => ({ team_id: teamId })); const result = await dbSyncFixed.smartDualInsertRelation( 'competition_teams', competitionId, slaveCompetitionId, relationData, 'team_id' ); if (!result.success) { console.error('添加競賽團隊失敗:', result.masterError || result.slaveError); return false; } } return true; } catch (error) { console.error('添加競賽團隊失敗:', error); return false; } } // 從競賽中移除團隊 static async removeCompetitionTeam(competitionId: string, teamId: string): Promise { const sql = 'DELETE FROM competition_teams WHERE competition_id = ? AND team_id = ?'; const result = await DatabaseServiceBase.safeDelete(sql, [competitionId, teamId]); return result.affectedRows > 0; } // 為競賽添加應用 static async addCompetitionApps(competitionId: string, appIds: string[]): Promise { try { const dbSyncFixed = new DatabaseSyncFixed(); // 先刪除現有的應用關聯 await DatabaseServiceBase.safeDelete('DELETE FROM competition_apps WHERE competition_id = ?', [competitionId]); // 添加新的應用關聯 if (appIds.length > 0) { // 獲取備機的競賽 ID - 通過名稱查找 const competition = await this.getCompetitionById(competitionId); const slaveCompetitionId = await this.getSlaveCompetitionIdByName(competition?.name || ''); if (!slaveCompetitionId) { console.error('找不到備機競賽 ID'); return false; } const relationData = appIds.map(appId => ({ app_id: appId })); const result = await dbSyncFixed.smartDualInsertRelation( 'competition_apps', competitionId, slaveCompetitionId, relationData, 'app_id' ); if (!result.success) { console.error('添加競賽應用失敗:', result.masterError || result.slaveError); return false; } } return true; } catch (error) { console.error('添加競賽應用失敗:', error); return false; } } // 從競賽中移除應用 static async removeCompetitionApp(competitionId: string, appId: string): Promise { const sql = 'DELETE FROM competition_apps WHERE competition_id = ? AND app_id = ?'; const result = await DatabaseServiceBase.safeDelete(sql, [competitionId, appId]); return result.affectedRows > 0; } // 獲取競賽的獎項類型列表 static async getCompetitionAwardTypes(competitionId: string): Promise { const sql = ` SELECT cat.* FROM competition_award_types cat WHERE cat.competition_id = ? ORDER BY cat.order_index ASC, cat.created_at ASC `; return await DatabaseServiceBase.safeQuery(sql, [competitionId]); } // 為競賽添加獎項類型 static async addCompetitionAwardTypes(competitionId: string, awardTypes: any[]): Promise { try { const dbSyncFixed = new DatabaseSyncFixed(); // 先刪除現有的獎項類型 await DatabaseServiceBase.safeDelete('DELETE FROM competition_award_types WHERE competition_id = ?', [competitionId]); // 添加新的獎項類型 if (awardTypes.length > 0) { // 獲取備機的競賽 ID - 通過名稱查找 const competition = await this.getCompetitionById(competitionId); const slaveCompetitionId = await this.getSlaveCompetitionIdByName(competition?.name || ''); if (!slaveCompetitionId) { console.error('找不到備機競賽 ID'); return false; } const relationData = awardTypes.map((awardType, i) => ({ name: awardType.name, description: awardType.description || '', icon: awardType.icon || '🏆', color: awardType.color || 'text-yellow-600', order_index: i })); const result = await dbSyncFixed.smartDualInsertRelation( 'competition_award_types', competitionId, slaveCompetitionId, relationData, 'name' ); if (!result.success) { console.error('添加競賽獎項類型失敗:', result.masterError || result.slaveError); return false; } } return true; } catch (error) { console.error('添加競賽獎項類型失敗:', error); return false; } } // 從競賽中移除獎項類型 static async removeCompetitionAwardType(competitionId: string, awardTypeId: string): Promise { const sql = 'DELETE FROM competition_award_types WHERE competition_id = ? AND id = ?'; const result = await DatabaseServiceBase.safeDelete(sql, [competitionId, awardTypeId]); return result.affectedRows > 0; } // 為競賽添加評分規則 static async addCompetitionRules(competitionId: string, rules: any[]): Promise { try { const dbSyncFixed = new DatabaseSyncFixed(); // 先刪除現有的評分規則 await DatabaseServiceBase.safeDelete('DELETE FROM competition_rules WHERE competition_id = ?', [competitionId]); // 添加新的評分規則 if (rules.length > 0) { // 獲取備機的競賽 ID - 通過名稱查找 const competition = await this.getCompetitionById(competitionId); const slaveCompetitionId = await this.getSlaveCompetitionIdByName(competition?.name || ''); if (!slaveCompetitionId) { console.error('找不到備機競賽 ID'); return false; } const relationData = rules.map((rule, i) => ({ name: rule.name, description: rule.description || '', weight: rule.weight || 0, order_index: i })); const result = await dbSyncFixed.smartDualInsertRelation( 'competition_rules', competitionId, slaveCompetitionId, relationData, 'name' ); if (!result.success) { console.error('添加競賽評分規則失敗:', result.masterError || result.slaveError); return false; } } return true; } catch (error) { console.error('添加競賽評分規則失敗:', error); return false; } } // 從競賽中移除評分規則 static async removeCompetitionRule(competitionId: string, ruleId: string): Promise { const sql = 'DELETE FROM competition_rules WHERE competition_id = ? AND id = ?'; const result = await DatabaseServiceBase.safeDelete(sql, [competitionId, ruleId]); return result.affectedRows > 0; } // 獲取競賽的完整信息(包含所有關聯數據) static async getCompetitionWithDetails(competitionId: string): Promise { const competition = await this.getCompetitionById(competitionId); if (!competition) return null; const [judges, teams, apps, awardTypes, rules] = await Promise.all([ this.getCompetitionJudges(competitionId), this.getCompetitionTeams(competitionId), this.getCompetitionApps(competitionId), this.getCompetitionAwardTypes(competitionId), ScoringService.getCompetitionRules(competitionId) ]); // 根據日期和評分進度動態計算競賽狀態 const now = new Date(); const startDate = new Date(competition.start_date); const endDate = new Date(competition.end_date); let calculatedStatus = competition.status; // 確保日期比較的準確性,使用 UTC 時間避免時區問題 const nowUTC = new Date(now.getTime() + now.getTimezoneOffset() * 60000); const startDateUTC = new Date(startDate.getTime() + startDate.getTimezoneOffset() * 60000); const endDateUTC = new Date(endDate.getTime() + endDate.getTimezoneOffset() * 60000); // 根據實際日期計算狀態 if (nowUTC < startDateUTC) { calculatedStatus = 'upcoming'; // 即將開始 } else if (nowUTC >= startDateUTC && nowUTC <= endDateUTC) { calculatedStatus = 'active'; // 進行中 } else if (nowUTC > endDateUTC) { // 競賽結束後,檢查評分進度 try { const scoringProgress = await ScoringService.getCompetitionScoringProgress(competitionId); if (scoringProgress.percentage >= 100) { calculatedStatus = 'completed'; // 評分完成,競賽完成 } else { calculatedStatus = 'judging'; // 評分未完成,仍在評審中 } } catch (error) { console.error('獲取評分進度失敗,使用預設狀態:', error); calculatedStatus = 'judging'; // 無法獲取進度時,預設為評審中 } } // 轉換字段名稱以匹配前端期望的格式 return { ...competition, status: calculatedStatus, // 使用計算後的狀態 startDate: competition.start_date, endDate: competition.end_date, evaluationFocus: competition.evaluation_focus, maxTeamSize: competition.max_team_size, isActive: competition.is_active, createdAt: competition.created_at, updatedAt: competition.updated_at, judges, teams, apps, awardTypes, rules }; } // 根據競賽ID獲取評審信息 static async getCompetitionJudges(competitionId: string): Promise { try { console.log('🔍 查詢競賽評審:', competitionId); // 先檢查 competition_judges 表是否有資料 const checkSql = `SELECT COUNT(*) as count FROM competition_judges WHERE competition_id = ?`; const checkResult = await db.query(checkSql, [competitionId]); console.log('📊 competition_judges 表中該競賽的記錄數:', checkResult[0]?.count || 0); // 檢查 judges 表是否有資料 const judgesCountSql = `SELECT COUNT(*) as count FROM judges WHERE is_active = TRUE`; const judgesCountResult = await db.query(judgesCountSql, []); console.log('📊 judges 表中活躍評審數:', judgesCountResult[0]?.count || 0); // 檢查關聯查詢 const joinCheckSql = ` SELECT cj.competition_id, cj.judge_id, j.id as judge_table_id, j.name, j.is_active FROM competition_judges cj LEFT JOIN judges j ON cj.judge_id = j.id WHERE cj.competition_id = ? `; const joinResult = await db.query(joinCheckSql, [competitionId]); console.log('🔗 關聯查詢結果:', joinResult); const sql = ` SELECT j.id, j.name, j.title, j.department, j.expertise, j.avatar, cj.assigned_at FROM competition_judges cj LEFT JOIN judges j ON cj.judge_id = j.id WHERE cj.competition_id = ? AND j.is_active = TRUE ORDER BY j.name `; console.log('📝 執行評審查詢SQL:', sql); console.log('📝 查詢參數:', [competitionId]); const result = await db.query(sql, [competitionId]); console.log('✅ 查詢評審結果:', result?.length || 0, '位評審'); console.log('👥 評審詳細結果:', result); return result; } catch (error) { console.error('❌ 查詢評審失敗:', error); throw error; } } } // ===================================================== // 應用服務 // ===================================================== export class AppService extends DatabaseServiceBase { // 創建應用 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 = 1 `; 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 = 1 `; 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 = 5 } = filters; // 構建查詢條件 let whereConditions: string[] = []; let params: any[] = []; // 根據狀態篩選 if (status && status !== 'all') { if (status === 'active') { whereConditions.push('a.is_active = 1'); } 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; department?: 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 && !updates.department) { return { success: false, error: '沒有要更新的欄位' }; } // 如果需要更新部門,先更新創建者的部門 if (updates.department) { // 先獲取應用的創建者 ID const appSql = 'SELECT creator_id FROM apps WHERE id = ? AND is_active = 1'; const appResult = await this.query(appSql, [appId]); if (appResult.length === 0) { return { success: false, error: '應用不存在' }; } const creatorId = appResult[0].creator_id; // 更新創建者的部門 const userUpdateSql = 'UPDATE users SET department = ? WHERE id = ?'; await this.query(userUpdateSql, [updates.department, creatorId]); } // 如果有其他欄位需要更新 if (updateFields.length > 0) { updateFields.push('updated_at = NOW()'); params.push(appId); const sql = ` UPDATE apps SET ${updateFields.join(', ')} WHERE id = ? AND is_active = 1 `; 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 = 1 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 this.safeQuery(sql, params); } // 通用單一查詢方法 - 使用安全查詢確保連線釋放 async queryOne(sql: string, params: any[] = []): Promise { return await this.safeQueryOne(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, startDate?: string, endDate?: string, department?: 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(*) 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(*) 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(*) 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]); // 部門使用統計 - 支援日期範圍過濾 let deptSql: string; let deptParams: any[]; if (startDate && endDate) { deptSql = ` SELECT u.department, COUNT(*) as count FROM user_views uv JOIN users u ON uv.user_id = u.id WHERE uv.app_id = ? AND DATE(uv.viewed_at) BETWEEN ? AND ? GROUP BY u.department ORDER BY count DESC LIMIT 5 `; deptParams = [appId, startDate, endDate]; } else { 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 `; deptParams = [appId]; } const deptResult = await this.query(deptSql, deptParams); // 使用趨勢 - 支援自定義日期範圍和部門過濾 let trendSql: string; let trendParams: any[]; // 構建部門過濾條件 const departmentFilter = department ? 'AND u.department = ?' : ''; const baseWhere = `uv.app_id = ? ${departmentFilter}`; if (startDate && endDate) { // 使用自定義日期範圍 trendSql = ` SELECT DATE(uv.viewed_at) as date, COUNT(*) as users FROM user_views uv JOIN users u ON uv.user_id = u.id WHERE ${baseWhere} AND DATE(uv.viewed_at) BETWEEN ? AND ? GROUP BY DATE(uv.viewed_at) ORDER BY date ASC `; trendParams = department ? [appId, department, startDate, endDate] : [appId, startDate, endDate]; } else { // 預設過去7天 trendSql = ` SELECT DATE(uv.viewed_at) as date, COUNT(*) as users FROM user_views uv JOIN users u ON uv.user_id = u.id WHERE ${baseWhere} AND uv.viewed_at >= DATE_SUB(CURDATE(), INTERVAL 7 DAY) GROUP BY DATE(uv.viewed_at) ORDER BY date ASC `; trendParams = department ? [appId, department] : [appId]; } const trendResult = await this.query(trendSql, trendParams); 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.comment, 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.comment || '用戶評價', // 使用 comment 欄位,如果為空則顯示預設文字 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 createAppReview(appId: string, userId: string, rating: number, comment: string): Promise { try { // 總是創建新評價,允許用戶對同一應用多次評論 const reviewId = `review_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const sql = ` INSERT INTO user_ratings (id, app_id, user_id, rating, comment, rated_at) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP) `; await this.query(sql, [reviewId, appId, userId, rating, comment]); return reviewId; } catch (error) { console.error('創建應用評論錯誤:', error); return null; } } // 更新應用評論 async updateAppReview(reviewId: string, appId: string, userId: string, rating: number, comment: string): Promise { try { // 檢查評論是否存在且屬於該用戶 const existingReview = await this.queryOne( 'SELECT id FROM user_ratings WHERE id = ? AND app_id = ? AND user_id = ?', [reviewId, appId, userId] ); if (!existingReview) { console.error('評論不存在或無權限修改'); return null; } // 更新評論 const sql = ` UPDATE user_ratings SET rating = ?, comment = ?, rated_at = CURRENT_TIMESTAMP WHERE id = ? AND app_id = ? AND user_id = ? `; await this.query(sql, [rating, comment, reviewId, appId, userId]); return reviewId; } catch (error) { console.error('更新應用評論錯誤:', error); return null; } } // 刪除評價 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: '刪除評價時發生錯誤' }; } } // 獲取評論投票統計 async getReviewVotes(reviewId: string, userId?: string): Promise<{ helpful: number; notHelpful: number; userVote?: boolean }> { try { const sql = ` SELECT SUM(CASE WHEN is_helpful = 1 THEN 1 ELSE 0 END) as helpful, SUM(CASE WHEN is_helpful = 0 THEN 1 ELSE 0 END) as not_helpful FROM review_votes WHERE review_id = ? `; const result = await this.queryOne(sql, [reviewId]); let userVote: boolean | undefined = undefined; if (userId) { const userVoteResult = await this.queryOne( 'SELECT is_helpful FROM review_votes WHERE review_id = ? AND user_id = ?', [reviewId, userId] ); if (userVoteResult) { // 確保正確的布林值轉換 userVote = Boolean(userVoteResult.is_helpful); } } return { helpful: result.helpful || 0, notHelpful: result.not_helpful || 0, userVote }; } catch (error) { console.error('獲取評論投票統計錯誤:', error); return { helpful: 0, notHelpful: 0 }; } } // 切換評論投票 async toggleReviewVote(reviewId: string, userId: string, isHelpful: boolean): Promise<{ success: boolean; error?: string }> { try { // 檢查是否已經投票 const existingVote = await this.queryOne( 'SELECT id, is_helpful FROM review_votes WHERE review_id = ? AND user_id = ?', [reviewId, userId] ); if (existingVote) { if (existingVote.is_helpful === isHelpful) { // 取消投票 await this.query('DELETE FROM review_votes WHERE id = ?', [existingVote.id]); } else { // 更改投票 await this.query( 'UPDATE review_votes SET is_helpful = ? WHERE id = ?', [isHelpful, existingVote.id] ); } } else { // 創建新投票 const voteId = `vote_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; await this.query( 'INSERT INTO review_votes (id, review_id, user_id, is_helpful) VALUES (?, ?, ?, ?)', [voteId, reviewId, userId, isHelpful] ); } return { success: true }; } catch (error) { console.error('切換評論投票錯誤:', error); return { success: false, error: '投票時發生錯誤' }; } } // 獲取應用部門列表 async getAppDepartments(): Promise<{ department: string; count: number }[]> { try { const sql = ` SELECT u.department, COUNT(DISTINCT a.id) as count FROM apps a JOIN users u ON a.creator_id = u.id WHERE a.is_active = 1 GROUP BY u.department ORDER BY count DESC, u.department ASC `; const departments = await this.query(sql); return departments.map((dept: any) => ({ department: dept.department, count: parseInt(dept.count) })); } catch (error) { console.error('獲取應用部門列表錯誤:', error); return []; } } // 獲取應用類型列表 async getAppTypes(): Promise<{ type: string; count: number }[]> { try { const sql = ` SELECT type, COUNT(*) as count FROM apps WHERE is_active = 1 GROUP BY type ORDER BY count DESC, type ASC `; const types = await this.query(sql); return types.map((type: any) => ({ type: type.type, count: parseInt(type.count) })); } catch (error) { console.error('獲取應用類型列表錯誤:', error); return []; } } // 獲取應用分類列表 async getAppCategories(): Promise<{ category: string; count: number }[]> { try { const sql = ` SELECT category, COUNT(*) as count FROM apps WHERE is_active = 1 GROUP BY category ORDER BY count DESC, category ASC `; const categories = await this.query(sql); return categories.map((cat: any) => ({ category: cat.category, count: parseInt(cat.count) })); } catch (error) { console.error('獲取應用分類列表錯誤:', error); return []; } } // 獲取應用統計數據 async getAppStats(appId: string, userId?: string): Promise { try { const sql = ` SELECT a.likes_count, a.views_count, COUNT(ur.id) as reviews_count, COALESCE(AVG(ur.rating), 0) as average_rating FROM apps a LEFT JOIN user_ratings ur ON a.id = ur.app_id WHERE a.id = ? GROUP BY a.id, a.likes_count, a.views_count `; const stats = await this.queryOne(sql, [appId]); let userLiked = false; if (userId) { const likeResult = await this.queryOne( 'SELECT id FROM user_likes WHERE app_id = ? AND user_id = ?', [appId, userId] ); userLiked = !!likeResult; } return { ...(stats || { likes_count: 0, views_count: 0, average_rating: 0, reviews_count: 0 }), userLiked }; } catch (error) { console.error('獲取應用統計數據錯誤:', error); return { likes_count: 0, views_count: 0, average_rating: 0, reviews_count: 0, userLiked: false }; } } // 切換應用按讚狀態 async toggleAppLike(appId: string, userId: string): Promise { try { // 檢查是否已經按讚 const existingLike = await this.queryOne( 'SELECT id FROM user_likes WHERE app_id = ? AND user_id = ?', [appId, userId] ); if (existingLike) { // 取消按讚 await this.query('DELETE FROM user_likes WHERE app_id = ? AND user_id = ?', [appId, userId]); // 更新應用按讚數 await this.query('UPDATE apps SET likes_count = GREATEST(likes_count - 1, 0) WHERE id = ?', [appId]); return false; // 已取消按讚 } else { // 添加按讚 await this.query( 'INSERT INTO user_likes (id, app_id, user_id, liked_at) VALUES (UUID(), ?, ?, NOW())', [appId, userId] ); // 更新應用按讚數 await this.query('UPDATE apps SET likes_count = likes_count + 1 WHERE id = ?', [appId]); return true; // 已按讚 } } catch (error) { console.error('切換應用按讚狀態錯誤:', error); return false; } } // 增加應用觀看次數 async incrementAppViews(appId: string, userId?: string): Promise { try { // 更新總瀏覽量 await this.query('UPDATE apps SET views_count = views_count + 1 WHERE id = ?', [appId]); // 如果有用戶ID,記錄到 user_views 表 if (userId) { await this.query( 'INSERT INTO user_views (id, user_id, app_id, viewed_at) VALUES (UUID(), ?, ?, NOW())', [userId, appId] ); } return true; } catch (error) { console.error('增加應用觀看次數錯誤:', error); return false; } } // 切換應用收藏狀態 async toggleAppFavorite(appId: string, userId: string): Promise { try { // 檢查是否已經收藏 const existingFavorite = await this.queryOne( 'SELECT id FROM user_favorites WHERE app_id = ? AND user_id = ?', [appId, userId] ); if (existingFavorite) { // 取消收藏 await this.query('DELETE FROM user_favorites WHERE app_id = ? AND user_id = ?', [appId, userId]); return false; // 已取消收藏 } else { // 添加收藏 await this.query( 'INSERT INTO user_favorites (id, app_id, user_id, favorited_at) VALUES (UUID(), ?, ?, NOW())', [appId, userId] ); return true; // 已收藏 } } catch (error) { console.error('切換應用收藏狀態錯誤:', error); return false; } } // 檢查用戶是否已按讚某應用 async hasUserLikedApp(appId: string, userId: string): Promise { try { const like = await this.queryOne( 'SELECT id FROM user_likes WHERE app_id = ? AND user_id = ?', [appId, userId] ); return !!like; } catch (error) { console.error('檢查用戶按讚狀態錯誤:', error); return false; } } // 檢查用戶是否已收藏某應用 async hasUserFavoritedApp(appId: string, userId: string): Promise { try { const favorite = await this.queryOne( 'SELECT id FROM user_favorites WHERE app_id = ? AND user_id = ?', [appId, userId] ); return !!favorite; } catch (error) { console.error('檢查用戶收藏狀態錯誤:', error); return false; } } // 獲取應用收藏數量 async getAppFavoritesCount(appId: string): Promise { try { const result = await this.queryOne( 'SELECT COUNT(*) as count FROM user_favorites WHERE app_id = ?', [appId] ); return result?.count || 0; } catch (error) { console.error('獲取應用收藏數量錯誤:', error); return 0; } } // 獲取儀表板統計數據 async getDashboardStats(): Promise<{ totalUsers: number; activeUsers: number; totalApps: number; activeApps: number; inactiveApps: number; pendingApps: number; totalCompetitions: number; totalReviews: number; totalViews: number; totalLikes: number; newAppsThisMonth: number; activeCompetitions: number; growthRate: number; }> { try { // 用戶統計 const userService = new UserService(); const userStats = await userService.getUserStats(); // 應用統計 const appStatsSql = ` SELECT COUNT(*) as total_apps, COUNT(CASE WHEN is_active = 1 THEN 1 END) as active_apps, COUNT(CASE WHEN is_active = FALSE THEN 1 END) as inactive_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 `; const appStats = await this.queryOne(appStatsSql); // 評論統計 const reviewStatsSql = ` SELECT COUNT(*) as total_reviews FROM user_ratings `; const reviewStats = await this.queryOne(reviewStatsSql); // 競賽統計 - 使用動態計算的狀態 const competitions = await CompetitionService.getAllCompetitions(); // 動態計算每個競賽的狀態 const now = new Date(); const competitionsWithCalculatedStatus = competitions.map(competition => { const startDate = new Date(competition.start_date); const endDate = new Date(competition.end_date); let calculatedStatus = competition.status; // 確保日期比較的準確性,使用 UTC 時間避免時區問題 const nowUTC = new Date(now.getTime() + now.getTimezoneOffset() * 60000); const startDateUTC = new Date(startDate.getTime() + startDate.getTimezoneOffset() * 60000); const endDateUTC = new Date(endDate.getTime() + endDate.getTimezoneOffset() * 60000); // 根據實際日期計算狀態 if (nowUTC < startDateUTC) { calculatedStatus = 'upcoming'; // 即將開始 } else if (nowUTC >= startDateUTC && nowUTC <= endDateUTC) { calculatedStatus = 'active'; // 進行中 } else if (nowUTC > endDateUTC) { calculatedStatus = 'completed'; // 已完成 } return { ...competition, status: calculatedStatus }; }); const competitionStats = { total_competitions: competitionsWithCalculatedStatus.length, active_competitions: competitionsWithCalculatedStatus.filter(c => c.status === 'active').length }; // 計算增長率(與上個月比較) 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 ? ((currentMonthUsers - lastMonthUsers.last_month_users) / lastMonthUsers.last_month_users) * 100 : 0; return { totalUsers: userStats.totalUsers, activeUsers: userStats.activeUsers, totalApps: appStats?.total_apps || 0, activeApps: appStats?.active_apps || 0, inactiveApps: appStats?.inactive_apps || 0, pendingApps: 0, // 目前沒有pending狀態的應用 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: Math.round(growthRate * 100) / 100 }; } catch (error) { console.error('獲取儀表板統計數據錯誤:', error); return { totalUsers: 0, activeUsers: 0, totalApps: 0, activeApps: 0, inactiveApps: 0, pendingApps: 0, totalCompetitions: 0, totalReviews: 0, totalViews: 0, totalLikes: 0, newAppsThisMonth: 0, activeCompetitions: 0, growthRate: 0 }; } } // 添加收藏 async addFavorite(userId: string, appId: string): Promise<{ success: boolean; error?: string }> { try { // 先檢查是否已經收藏 const isAlreadyFavorited = await this.isFavorited(userId, appId); if (isAlreadyFavorited) { return { success: false, error: '已經收藏過此應用' }; } const favoriteId = crypto.randomUUID(); const sql = ` INSERT INTO user_favorites (id, user_id, app_id, created_at) VALUES (?, ?, ?, NOW()) `; await this.query(sql, [favoriteId, userId, appId]); return { success: true }; } catch (error: any) { if (error.code === 'ER_DUP_ENTRY') { // 如果仍然出現重複鍵錯誤,嘗試清理可能的重複記錄 try { await this.removeFavorite(userId, appId); const favoriteId = crypto.randomUUID(); const sql = ` INSERT INTO user_favorites (id, user_id, app_id, created_at) VALUES (?, ?, ?, NOW()) `; await this.query(sql, [favoriteId, userId, appId]); return { success: true }; } catch (retryError) { console.error('重試添加收藏錯誤:', retryError); return { success: false, error: '已經收藏過此應用' }; } } console.error('添加收藏錯誤:', error); return { success: false, error: '添加收藏時發生錯誤' }; } } // 移除收藏 async removeFavorite(userId: string, appId: string): Promise<{ success: boolean; error?: string }> { try { const sql = ` DELETE FROM user_favorites WHERE user_id = ? AND app_id = ? `; const result = await this.query(sql, [userId, appId]); return { success: true }; } catch (error) { console.error('移除收藏錯誤:', error); return { success: false, error: '移除收藏時發生錯誤' }; } } // 檢查是否已收藏 async isFavorited(userId: string, appId: string): Promise { try { const sql = ` SELECT COUNT(*) as count FROM user_favorites WHERE user_id = ? AND app_id = ? `; const result = await this.queryOne(sql, [userId, appId]); return result.count > 0; } catch (error) { console.error('檢查收藏狀態錯誤:', error); return false; } } // 獲取用戶收藏的應用列表 async getUserFavorites(userId: string, limit: number = 20, offset: number = 0): Promise<{ apps: any[]; total: number }> { try { // 獲取總數 const countSql = ` SELECT COUNT(*) as total FROM user_favorites uf JOIN apps a ON uf.app_id = a.id WHERE uf.user_id = ? AND a.is_active = 1 `; const countResult = await this.queryOne(countSql, [userId]); const total = countResult?.total || 0; // 獲取收藏的應用列表 - 包含真實的評分和評論數據 const sql = ` SELECT a.*, u.name as creator_name, u.department as creator_department, uf.created_at as favorited_at, COALESCE(( SELECT AVG(rating) FROM user_ratings WHERE app_id = a.id ), 0) as rating, COALESCE(( SELECT COUNT(*) FROM user_ratings WHERE app_id = a.id ), 0) as reviewCount FROM user_favorites uf JOIN apps a ON uf.app_id = a.id LEFT JOIN users u ON a.creator_id = u.id WHERE uf.user_id = ? AND a.is_active = 1 ORDER BY uf.created_at DESC LIMIT ${limit} OFFSET ${offset} `; const apps = await this.query(sql, [userId]); return { apps: apps.map(app => ({ id: app.id, name: app.name, description: app.description, category: app.category, type: app.type, views: app.views_count || 0, likes: app.likes_count || 0, rating: app.rating || 0, reviewCount: app.reviewCount || 0, creator: app.creator_name || '未知', department: app.creator_department || '未知', createdAt: app.created_at ? new Date(app.created_at).toLocaleDateString('zh-TW') : '-', icon: app.icon || 'Bot', iconColor: app.icon_color || 'from-blue-500 to-purple-500', appUrl: app.app_url, favoritedAt: app.favorited_at })), total }; } catch (error) { console.error('獲取用戶收藏列表錯誤:', error); return { apps: [], total: 0 }; } } // 獲取用戶收藏的應用ID列表(不依賴 apps 表狀態) async getUserFavoriteAppIds(userId: string): Promise { try { const sql = ` SELECT app_id FROM user_favorites WHERE user_id = ? ORDER BY created_at DESC `; const result = await this.query(sql, [userId]); return result.map((row: any) => row.app_id); } catch (error) { console.error('獲取用戶收藏應用ID列表錯誤:', error); return []; } } // 獲取用戶按讚的應用列表 async getUserLikedApps(userId: string): Promise { try { const sql = ` SELECT DISTINCT app_id, MAX(liked_at) as latest_liked_at FROM user_likes WHERE user_id = ? GROUP BY app_id ORDER BY latest_liked_at DESC `; const result = await this.query(sql, [userId]); return result.map((row: any) => row.app_id); } catch (error) { console.error('獲取用戶按讚應用列表錯誤:', error); return []; } } // 檢查用戶是否按讚了特定應用 async isLiked(userId: string, appId: string): Promise { try { const sql = ` SELECT COUNT(*) as count FROM user_likes WHERE user_id = ? AND app_id = ? `; const result = await this.queryOne(sql, [userId, appId]); return (result?.count || 0) > 0; } catch (error) { console.error('檢查按讚狀態錯誤:', error); return false; } } // 獲取用戶最近使用的應用 async getUserRecentApps(userId: string, limit: number = 10): Promise { try { // 直接從活動記錄獲取數據,不依賴應用表 const activitySql = ` SELECT al.resource_id, al.created_at as last_used, al.details FROM activity_logs al WHERE al.user_id = ? AND al.action = 'view' AND al.resource_type = 'app' ORDER BY al.created_at DESC LIMIT ${limit} `; const activities = await this.query(activitySql, [userId]); // 統計每個應用的使用次數和最新使用時間 const appUsageMap = new Map(); // 先統計使用次數 activities.forEach((activity) => { const appId = activity.resource_id; if (appUsageMap.has(appId)) { // 更新使用次數和最新時間 const existing = appUsageMap.get(appId); existing.usageCount += 1; if (new Date(activity.last_used) > new Date(existing.lastUsed)) { existing.lastUsed = activity.last_used; } } else { // 新增應用記錄(暫時使用基本信息) appUsageMap.set(appId, { id: appId, lastUsed: activity.last_used, usageCount: 1 }); } }); // 為每個應用獲取詳細信息 const apps = []; for (const [appId, usageInfo] of appUsageMap) { try { // 從 apps 表獲取應用詳細信息 const appSql = ` 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 = ? `; const appResult = await this.query(appSql, [appId]); if (appResult.length > 0) { const app = appResult[0]; apps.push({ id: app.id, name: app.name, description: app.description || '應用描述不可用', category: app.category || '未分類', type: app.type || '未分類', creator: app.creator_name || '未知創建者', department: app.creator_department || '未知部門', icon: app.icon || 'MessageSquare', iconColor: app.icon_color || 'from-blue-500 to-purple-500', appUrl: app.app_url || '#', lastUsed: usageInfo.lastUsed, usageCount: usageInfo.usageCount }); } else { // 如果應用不存在,使用活動記錄中的信息作為備用 const activity = activities.find(a => a.resource_id === appId); let appDetails = {}; if (activity && activity.details) { if (typeof activity.details === 'string') { try { appDetails = JSON.parse(activity.details); } catch (error) { appDetails = {}; } } else if (typeof activity.details === 'object') { appDetails = activity.details; } } apps.push({ id: appId, name: (appDetails as any).appName || '未知應用', description: '應用描述不可用', category: '未分類', type: '未分類', creator: '未知創建者', department: '未知部門', icon: 'MessageSquare', iconColor: 'from-blue-500 to-purple-500', appUrl: '#', lastUsed: usageInfo.lastUsed, usageCount: usageInfo.usageCount }); } } catch (error) { console.error('獲取應用詳細信息錯誤:', error); // 出錯時使用基本信息 apps.push({ id: appId, name: '未知應用', description: '應用描述不可用', category: '未分類', type: '未分類', creator: '未知創建者', department: '未知部門', icon: 'MessageSquare', iconColor: 'from-blue-500 to-purple-500', appUrl: '#', lastUsed: usageInfo.lastUsed, usageCount: usageInfo.usageCount }); } } // 按最新使用時間排序 return apps.sort((a, b) => new Date(b.lastUsed).getTime() - new Date(a.lastUsed).getTime()); } catch (error) { console.error('獲取用戶最近使用應用錯誤:', error); return []; } } // 獲取用戶活動統計 async getUserActivityStats(userId: string): Promise { try { // 獲取總使用次數 const usageSql = ` SELECT COUNT(*) as total_usage FROM activity_logs WHERE user_id = ? AND action = 'view' AND resource_type = 'app' `; const usageResult = await this.queryOne(usageSql, [userId]); const totalUsage = usageResult?.total_usage || 0; // 獲取收藏應用數量 const favoritesSql = ` SELECT COUNT(*) as favorite_count FROM user_favorites WHERE user_id = ? `; const favoritesResult = await this.queryOne(favoritesSql, [userId]); const favoriteApps = favoritesResult?.favorite_count || 0; // 獲取用戶加入天數 const userSql = ` SELECT join_date FROM users WHERE id = ? `; const userResult = await this.queryOne(userSql, [userId]); const joinDate = userResult?.join_date; let daysJoined = 0; if (joinDate) { const join = new Date(joinDate); const now = new Date(); daysJoined = Math.floor((now.getTime() - join.getTime()) / (1000 * 60 * 60 * 24)); } return { totalUsage, favoriteApps, daysJoined: Math.max(0, daysJoined) }; } catch (error) { console.error('獲取用戶活動統計錯誤:', error); return { totalUsage: 0, favoriteApps: 0, daysJoined: 0 }; } } // 獲取用戶類別使用統計 async getUserCategoryStats(userId: string): Promise { try { // 先獲取活動記錄,再統計類別 const activitySql = ` SELECT al.resource_id, al.details FROM activity_logs al WHERE al.user_id = ? AND al.action = 'view' AND al.resource_type = 'app' ORDER BY al.created_at DESC `; const activities = await this.query(activitySql, [userId]); // 統計類別使用次數 const categoryCount: { [key: string]: { count: number; uniqueApps: Set } } = {}; for (const activity of activities) { try { // 嘗試從應用表獲取類別 const appSql = 'SELECT type FROM apps WHERE id = ? AND is_active = 1'; const appResult = await this.query(appSql, [activity.resource_id]); let category = '未分類'; if (appResult.length > 0) { category = appResult[0].type || '未分類'; } else { // 如果應用不存在,嘗試從 details 中獲取 let details = {}; if (activity.details) { if (typeof activity.details === 'string') { try { details = JSON.parse(activity.details); } catch (error) { details = {}; } } else if (typeof activity.details === 'object') { details = activity.details; } } category = (details as any).category || '未分類'; } if (!categoryCount[category]) { categoryCount[category] = { count: 0, uniqueApps: new Set() }; } categoryCount[category].count++; categoryCount[category].uniqueApps.add(activity.resource_id); } catch (error) { // 出錯時使用默認類別 const defaultCategory = '未分類'; if (!categoryCount[defaultCategory]) { categoryCount[defaultCategory] = { count: 0, uniqueApps: new Set() }; } categoryCount[defaultCategory].count++; categoryCount[defaultCategory].uniqueApps.add(activity.resource_id); } } // 轉換為數組並排序 const results = Object.entries(categoryCount) .map(([category, data]) => ({ category, usage_count: data.count, unique_apps: data.uniqueApps.size })) .sort((a, b) => b.usage_count - a.usage_count) .slice(0, 10); const totalUsage = results.reduce((sum, item) => sum + item.usage_count, 0); return results.map((item, index) => ({ name: item.category, usage: totalUsage > 0 ? Math.round((item.usage_count / totalUsage) * 100) : 0, count: item.usage_count, uniqueApps: item.unique_apps, color: this.getCategoryColor(item.category, index) })); } catch (error) { console.error('獲取用戶類別統計錯誤:', error); return []; } } // 記錄用戶活動 async logUserActivity(activityData: { userId: string; action: string; resourceType: string; resourceId?: string; details?: any; }): Promise { try { const activityId = crypto.randomUUID(); const sql = ` INSERT INTO activity_logs (id, user_id, action, resource_type, resource_id, details, created_at) VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) `; await this.query(sql, [ activityId, activityData.userId, activityData.action, activityData.resourceType, activityData.resourceId || null, activityData.details ? JSON.stringify(activityData.details) : null ]); return activityId; } catch (error) { console.error('記錄用戶活動錯誤:', error); throw error; } } // 獲取類別顏色(輔助方法) private getCategoryColor(category: string, index: number): string { const colors = [ 'bg-blue-500', 'bg-green-500', 'bg-purple-500', 'bg-orange-500', 'bg-pink-500', 'bg-indigo-500', 'bg-cyan-500', 'bg-teal-500', 'bg-yellow-500', 'bg-red-500' ]; return colors[index % colors.length]; } } // ===================================================== // 評分服務 // ===================================================== export class ScoringService extends DatabaseServiceBase { // 獲取競賽規則 static async getCompetitionRules(competitionId: string): Promise { console.log('🔍 獲取競賽規則,competitionId:', competitionId); const sql = ` SELECT id, name, description, weight, order_index FROM competition_rules WHERE competition_id = ? ORDER BY order_index ASC `; try { const result = await DatabaseServiceBase.safeQuery(sql, [competitionId]); return result; } catch (error) { console.error('❌ 獲取競賽規則失敗:', error); return []; } } // 根據APP ID獲取競賽ID static async getCompetitionIdByAppId(appId: string): Promise { const sql = ` SELECT ca.competition_id FROM competition_apps ca WHERE ca.app_id = ? `; const result = await DatabaseServiceBase.safeQuery(sql, [appId]); return result.length > 0 ? result[0].competition_id : null; } // 提交應用評分(基於競賽規則的動態評分) static async submitAppScore(scoreData: any): Promise { console.log('🔍 開始提交評分,數據:', scoreData); const { judge_id, app_id, competition_id, scores, total_score, comments, isEdit, recordId } = scoreData; // 驗證必要參數 console.log('🔍 參數驗證:'); console.log('judge_id:', judge_id, typeof judge_id); console.log('app_id:', app_id, typeof app_id); console.log('competition_id:', competition_id, typeof competition_id); console.log('scores:', scores, typeof scores); console.log('total_score:', total_score, typeof total_score); if (!judge_id || !app_id || !competition_id || !scores || total_score === undefined) { throw new Error('缺少必要的評分參數'); } try { // 1. 生成唯一的評分記錄ID const judgeScoreId = `js_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; console.log('🔍 生成的評分記錄ID:', judgeScoreId); let finalJudgeScoreId; if (isEdit && recordId) { // 編輯模式:使用傳入的記錄ID finalJudgeScoreId = recordId; console.log('🔍 編輯模式,使用記錄ID:', finalJudgeScoreId); } else { // 新增模式:檢查是否已存在評分記錄 const existingScore = await DatabaseServiceBase.safeQuery( 'SELECT id FROM judge_scores WHERE judge_id = ? AND app_id = ?', [judge_id, app_id] ); if (existingScore.length > 0) { // 使用現有的評分記錄ID finalJudgeScoreId = existingScore[0].id; console.log('🔍 使用現有評分記錄ID:', finalJudgeScoreId); } else { // 創建新記錄 finalJudgeScoreId = `js_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; console.log('🔍 創建新評分記錄ID:', finalJudgeScoreId); } } // 檢查記錄是否存在,決定是更新還是插入 const existingRecord = await DatabaseServiceBase.safeQuery( 'SELECT id FROM judge_scores WHERE id = ?', [finalJudgeScoreId] ); if (existingRecord.length > 0) { // 更新現有記錄 await DatabaseServiceBase.safeUpdate(` UPDATE judge_scores SET total_score = ?, comments = ?, submitted_at = CURRENT_TIMESTAMP WHERE id = ? `, [total_score, comments || null, finalJudgeScoreId]); console.log('🔍 更新現有評分記錄'); } else { // 檢查是否已有相同 judge_id + app_id + competition_id 的記錄 const duplicateRecord = await DatabaseServiceBase.safeQuery( 'SELECT id FROM judge_scores WHERE judge_id = ? AND app_id = ? AND competition_id = ?', [judge_id, app_id, competition_id] ); if (duplicateRecord.length > 0) { // 更新現有記錄 finalJudgeScoreId = duplicateRecord[0].id; await DatabaseServiceBase.safeUpdate(` UPDATE judge_scores SET total_score = ?, comments = ?, submitted_at = CURRENT_TIMESTAMP WHERE id = ? `, [total_score, comments || null, finalJudgeScoreId]); console.log('🔍 更新重複的評分記錄'); } else { // 創建新記錄 await DatabaseServiceBase.safeInsert(` INSERT INTO judge_scores (id, judge_id, app_id, competition_id, total_score, comments) VALUES (?, ?, ?, ?, ?, ?) `, [ finalJudgeScoreId, judge_id, app_id, competition_id, total_score, comments || null ]); console.log('🔍 創建新評分記錄'); } } // 2. 獲取競賽規則 const rules = await this.getCompetitionRules(competition_id); console.log('🔍 競賽規則:', rules); // 3. 刪除現有的評分詳情 await DatabaseServiceBase.safeDelete( 'DELETE FROM judge_score_details WHERE judge_score_id = ?', [finalJudgeScoreId] ); // 4. 插入新的評分詳情 for (const [ruleName, score] of Object.entries(scores)) { if (typeof score === 'number' && score > 0) { // 找到對應的規則 const rule = rules.find((r: any) => r.name === ruleName); console.log(`🔍 尋找規則 ${ruleName}:`, rule); if (rule) { const detailId = `jsd_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; console.log('🔍 插入評分詳情:', { detailId, finalJudgeScoreId, ruleId: rule.id, ruleName: rule.name, score, weight: rule.weight }); await DatabaseServiceBase.safeInsert(` INSERT INTO judge_score_details (id, judge_score_id, rule_id, rule_name, score, weight) VALUES (?, ?, ?, ?, ?, ?) `, [ detailId, finalJudgeScoreId, rule.id, rule.name, score, rule.weight ]); } else { console.log(`⚠️ 找不到規則: ${ruleName}`); } } } // 返回完整的評分記錄 return await this.getJudgeScoreById(finalJudgeScoreId); } catch (error) { console.error('❌ 提交評分失敗:', error); throw error; } } // 根據ID獲取評分記錄 static async getJudgeScoreById(judgeScoreId: string): Promise { const sql = ` SELECT js.*, GROUP_CONCAT( CONCAT(jsd.rule_name, ':', jsd.score, ':', jsd.weight) SEPARATOR '|' ) as score_details FROM judge_scores js LEFT JOIN judge_score_details jsd ON js.id = jsd.judge_score_id WHERE js.id = ? GROUP BY js.id `; const result = await DatabaseServiceBase.safeQuery(sql, [judgeScoreId]); return result.length > 0 ? result[0] : null; } // 獲取競賽評分進度 static async getCompetitionScoringProgress(competitionId: string): Promise<{ completed: number; total: number; percentage: number; appCount: number; }> { try { console.log('🔍 獲取競賽評分進度,competitionId:', competitionId); // 獲取競賽的參賽APP數量(從 competition_teams 與 apps 串聯) const appsResult = await DatabaseServiceBase.safeQuery(` SELECT COUNT(DISTINCT a.id) as app_count FROM competition_teams ct INNER JOIN apps a ON ct.team_id = a.team_id WHERE ct.competition_id = ? AND a.is_active = 1 `, [competitionId]); const appCount = appsResult[0]?.app_count || 0; console.log('🔍 參賽APP數量 (從 competition_teams):', appCount); // 獲取競賽的評分項目數量(從 competition_rules) const rulesResult = await DatabaseServiceBase.safeQuery(` SELECT COUNT(*) as rule_count FROM competition_rules WHERE competition_id = ? `, [competitionId]); const ruleCount = rulesResult[0]?.rule_count || 0; console.log('🔍 評分項目數量:', ruleCount); // 獲取已完成的評分數量 const completedResult = await DatabaseServiceBase.safeQuery(` SELECT COUNT(*) as completed_count FROM judge_scores js WHERE js.competition_id = ? `, [competitionId]); const completed = completedResult[0]?.completed_count || 0; // 計算總評分數:參賽APP數量 × 評分項目數量 const total = appCount * ruleCount; // 計算進度百分比,確保不超過100% const percentage = total > 0 ? Math.min(Math.round((completed / total) * 100), 100) : 0; console.log('🔍 進度計算詳情:', { appCount, ruleCount, total, completed, percentage }); console.log('🔍 評分進度結果:', { completed, total, percentage }); return { completed, total, percentage, appCount }; } catch (error) { console.error('獲取評分進度失敗:', error); return { completed: 0, total: 0, percentage: 0, appCount: 0 }; } } // 獲取評分完成度匯總 static async getScoringSummary(competitionId: string): Promise<{ judges: Array<{ id: string; name: string; email: string; completedCount: number; totalCount: number; completionRate: number; status: 'completed' | 'partial' | 'not_started'; lastScoredAt?: string; }>; apps: Array<{ id: string; name: string; teamName?: string; scoredCount: number; totalJudges: number; completionRate: number; status: 'completed' | 'partial' | 'not_scored'; averageScore?: number; }>; overallStats: { totalJudges: number; totalApps: number; totalPossibleScores: number; completedScores: number; overallCompletionRate: number; }; }> { try { // 獲取競賽的評審列表 - 先嘗試從關聯表獲取,如果沒有則獲取所有評審 let judgesResult = await DatabaseServiceBase.safeQuery(` SELECT j.id, j.name FROM judges j LEFT JOIN competition_judges cj ON j.id = cj.judge_id WHERE cj.competition_id = ? ORDER BY j.name `, [competitionId]); // 如果沒有關聯的評審,獲取所有評審 if (judgesResult.length === 0) { judgesResult = await DatabaseServiceBase.safeQuery(` SELECT id, name FROM judges ORDER BY name `); } // 獲取競賽的APP列表 - 先嘗試從關聯表獲取,如果沒有則獲取所有APP let appsResult = await DatabaseServiceBase.safeQuery(` SELECT a.id, a.name, t.name as team_name FROM apps a LEFT JOIN competition_apps ca ON a.id = ca.app_id LEFT JOIN teams t ON a.team_id = t.id WHERE ca.competition_id = ? ORDER BY a.name `, [competitionId]); // 如果沒有關聯的APP,獲取所有APP if (appsResult.length === 0) { appsResult = await DatabaseServiceBase.safeQuery(` SELECT a.id, a.name, t.name as team_name FROM apps a LEFT JOIN teams t ON a.team_id = t.id ORDER BY a.name `); } // 獲取已完成的評分記錄 const scoresResult = await DatabaseServiceBase.safeQuery(` SELECT js.judge_id, js.app_id, js.total_score, js.submitted_at, j.name as judge_name, a.name as app_name FROM judge_scores js LEFT JOIN judges j ON js.judge_id = j.id LEFT JOIN apps a ON js.app_id = a.id WHERE js.competition_id = ? ORDER BY js.submitted_at DESC `, [competitionId]); const judges = judgesResult.map((judge: any) => { const judgeScores = scoresResult.filter((score: any) => score.judge_id === judge.id); const completedCount = judgeScores.length; const totalCount = appsResult.length; const completionRate = totalCount > 0 ? Math.min(Math.round((completedCount / totalCount) * 100), 100) : 0; let status: 'completed' | 'partial' | 'not_started' = 'not_started'; if (completedCount === totalCount && totalCount > 0) status = 'completed'; else if (completedCount > 0) status = 'partial'; const lastScoredAt = judgeScores.length > 0 ? judgeScores[0].submitted_at : undefined; return { id: judge.id, name: judge.name, email: '', // judges 表沒有 email 字段 completedCount, totalCount, completionRate, status, lastScoredAt }; }); const apps = appsResult.map((app: any) => { const appScores = scoresResult.filter((score: any) => score.app_id === app.id); const scoredCount = appScores.length; const totalJudges = judges.length; const completionRate = totalJudges > 0 ? Math.min(Math.round((scoredCount / totalJudges) * 100), 100) : 0; let status: 'completed' | 'partial' | 'not_scored' = 'not_scored'; if (scoredCount >= totalJudges && totalJudges > 0) status = 'completed'; else if (scoredCount > 0) status = 'partial'; const averageScore = appScores.length > 0 ? Math.round(appScores.reduce((sum: number, score: any) => sum + (score.total_score || 0), 0) / appScores.length) : undefined; return { id: app.id, name: app.name, teamName: app.team_name, scoredCount, totalJudges, completionRate, status, averageScore }; }); const totalJudges = judges.length; const totalApps = apps.length; const totalPossibleScores = totalJudges * totalApps; const completedScores = scoresResult.length; const overallCompletionRate = totalPossibleScores > 0 ? Math.min(Math.round((completedScores / totalPossibleScores) * 100), 100) : 0; const summary = { judges, apps, overallStats: { totalJudges, totalApps, totalPossibleScores, completedScores, overallCompletionRate } }; console.log('🔍 評分完成度匯總結果:', summary); return summary; } catch (error) { console.error('獲取評分完成度匯總失敗:', error); return { judges: [], apps: [], overallStats: { totalJudges: 0, totalApps: 0, totalPossibleScores: 0, completedScores: 0, overallCompletionRate: 0 } }; } } // 獲取應用評分 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 DatabaseServiceBase.safeInsert(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]); } // 獲取競賽的所有評分記錄(包含評審和參賽者信息) static async getCompetitionScores(competitionId: string): Promise { const sql = ` SELECT js.id, js.judge_id, js.app_id, js.total_score, js.comments, js.submitted_at, j.name as judge_name, j.title as judge_title, j.department as judge_department, a.name as app_name, a.creator_id, u.name as creator_name, u.department as creator_department, 'app' as participant_type, -- 從 judge_score_details 表獲取詳細評分 (SELECT GROUP_CONCAT(CONCAT(jsd.rule_name, ':', jsd.score) SEPARATOR ',') FROM judge_score_details jsd WHERE jsd.judge_score_id = js.id) as score_details FROM judge_scores js JOIN judges j ON js.judge_id = j.id JOIN apps a ON js.app_id = a.id JOIN users u ON a.creator_id = u.id WHERE js.competition_id = ? UNION ALL SELECT js.id, js.judge_id, js.app_id, js.total_score, js.comments, js.submitted_at, j.name as judge_name, j.title as judge_title, j.department as judge_department, t.name as app_name, t.leader_id as creator_id, u.name as creator_name, u.department as creator_department, 'team' as participant_type, -- 從 judge_score_details 表獲取詳細評分 (SELECT GROUP_CONCAT(CONCAT(jsd.rule_name, ':', jsd.score) SEPARATOR ',') FROM judge_score_details jsd WHERE jsd.judge_score_id = js.id) as score_details FROM judge_scores js JOIN judges j ON js.judge_id = j.id JOIN teams t ON js.app_id = t.id JOIN users u ON t.leader_id = u.id WHERE js.competition_id = ? UNION ALL SELECT js.id, js.judge_id, js.app_id, js.total_score, js.comments, js.submitted_at, j.name as judge_name, j.title as judge_title, j.department as judge_department, p.title as app_name, p.team_id as creator_id, t.name as creator_name, t.department as creator_department, 'proposal' as participant_type, -- 從 judge_score_details 表獲取詳細評分 (SELECT GROUP_CONCAT(CONCAT(jsd.rule_name, ':', jsd.score) SEPARATOR ',') FROM judge_score_details jsd WHERE jsd.judge_score_id = js.id) as score_details FROM judge_scores js JOIN judges j ON js.judge_id = j.id JOIN proposals p ON js.app_id = p.id JOIN teams t ON p.team_id = t.id WHERE js.competition_id = ? ORDER BY submitted_at DESC `; return await DatabaseServiceBase.safeQuery(sql, [competitionId, competitionId, competitionId]); } // 提交團隊評分(使用應用評分表,但標記為團隊類型) static async submitTeamScore(scoreData: Omit & { teamId: string }): Promise { // 創建一個虛擬的應用ID來存儲團隊評分 // 格式:team_{teamId} 以便識別這是團隊評分 const virtualAppId = `team_${scoreData.teamId}`; 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, virtualAppId, // 使用虛擬應用ID scoreData.innovation_score, scoreData.technical_score, scoreData.usability_score, scoreData.presentation_score, scoreData.impact_score, scoreData.total_score, scoreData.comments || null ]; await DatabaseServiceBase.safeInsert(sql, params); return await this.getAppScore(scoreData.judge_id, virtualAppId) as AppJudgeScore; } // 獲取競賽評分統計 static async getCompetitionScoreStats(competitionId: string): Promise<{ totalScores: number; completedScores: number; pendingScores: number; completionRate: number; totalParticipants: number; }> { // 獲取競賽參與者數量 const participantsSql = ` SELECT (SELECT COUNT(*) FROM competition_apps WHERE competition_id = ?) + (SELECT COUNT(*) FROM competition_teams WHERE competition_id = ?) + (SELECT COUNT(*) FROM competition_proposals WHERE competition_id = ?) as total_participants `; const participantsResult = await db.queryOne<{ total_participants: number }>(participantsSql, [competitionId, competitionId, competitionId]); const totalParticipants = participantsResult?.total_participants || 0; // 獲取評審數量 const judgesSql = 'SELECT COUNT(*) as judge_count FROM competition_judges WHERE competition_id = ?'; const judgesResult = await db.queryOne<{ judge_count: number }>(judgesSql, [competitionId]); const judgeCount = judgesResult?.judge_count || 0; // 計算總評分項目數 const totalScores = totalParticipants * judgeCount; // 獲取已完成評分數量 const completedScoresSql = ` SELECT COUNT(*) as completed_count FROM judge_scores WHERE competition_id = ? `; const completedResult = await db.queryOne<{ completed_count: number }>(completedScoresSql, [competitionId]); const completedScores = completedResult?.completed_count || 0; const pendingScores = Math.max(0, totalScores - completedScores); const completionRate = totalScores > 0 ? Math.round((completedScores / totalScores) * 100) : 0; return { totalScores, completedScores, pendingScores, completionRate, totalParticipants }; } // 獲取評審的評分記錄 static async getJudgeScores(judgeId: string, competitionId?: string): Promise { let sql = ` SELECT ajs.id, ajs.judge_id, ajs.app_id, ajs.innovation_score, ajs.technical_score, ajs.usability_score, ajs.presentation_score, ajs.impact_score, ajs.total_score, ajs.comments, ajs.submitted_at, a.name as app_name, a.creator_id, u.name as creator_name, u.department as creator_department, 'app' as participant_type FROM app_judge_scores ajs JOIN apps a ON ajs.app_id = a.id JOIN users u ON a.creator_id = u.id WHERE ajs.judge_id = ? `; const params = [judgeId]; if (competitionId) { sql += ' AND EXISTS (SELECT 1 FROM competition_apps ca WHERE ca.app_id = a.id AND ca.competition_id = ?)'; params.push(competitionId); } sql += ` UNION ALL SELECT pjs.id, pjs.judge_id, pjs.proposal_id as app_id, pjs.problem_identification_score as innovation_score, pjs.solution_feasibility_score as technical_score, pjs.innovation_score as usability_score, pjs.impact_score as presentation_score, pjs.presentation_score as impact_score, pjs.total_score, pjs.comments, pjs.submitted_at, p.title as app_name, p.team_id as creator_id, t.name as creator_name, t.department as creator_department, 'proposal' as participant_type FROM proposal_judge_scores pjs JOIN proposals p ON pjs.proposal_id = p.id JOIN teams t ON p.team_id = t.id WHERE pjs.judge_id = ? `; params.push(judgeId); if (competitionId) { sql += ' AND EXISTS (SELECT 1 FROM competition_proposals cp WHERE cp.proposal_id = p.id AND cp.competition_id = ?)'; params.push(competitionId); } sql += ' ORDER BY submitted_at DESC'; return await DatabaseServiceBase.safeQuery(sql, params); } // 刪除評分記錄 static async deleteScore(scoreId: string, scoreType: 'app' | 'proposal'): Promise { const tableName = scoreType === 'app' ? 'app_judge_scores' : 'proposal_judge_scores'; const sql = `DELETE FROM ${tableName} WHERE id = ?`; const result = await DatabaseServiceBase.safeDelete(sql, [scoreId]); return result.affectedRows > 0; } // 更新評分記錄 static async updateAppScore(scoreId: string, updates: Partial): Promise { const fields = Object.keys(updates).filter(key => key !== 'id' && key !== 'judge_id' && key !== 'app_id' && key !== 'submitted_at' ); if (fields.length === 0) return true; const setClause = fields.map(field => `${field} = ?`).join(', '); const values = fields.map(field => (updates as any)[field]); const sql = `UPDATE app_judge_scores SET ${setClause}, submitted_at = CURRENT_TIMESTAMP WHERE id = ?`; const result = await DatabaseServiceBase.safeUpdate(sql, [...values, scoreId]); return result.affectedRows > 0; } // 更新提案評分記錄 static async updateProposalScore(scoreId: string, updates: Partial): Promise { const fields = Object.keys(updates).filter(key => key !== 'id' && key !== 'judge_id' && key !== 'proposal_id' && key !== 'submitted_at' ); if (fields.length === 0) return true; const setClause = fields.map(field => `${field} = ?`).join(', '); const values = fields.map(field => (updates as any)[field]); const sql = `UPDATE proposal_judge_scores SET ${setClause}, submitted_at = CURRENT_TIMESTAMP WHERE id = ?`; const result = await DatabaseServiceBase.safeUpdate(sql, [...values, scoreId]); return result.affectedRows > 0; } } // ===================================================== // 獎項服務 // ===================================================== export class AwardService extends DatabaseServiceBase { // 創建獎項 static async createAward(awardData: any): 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, description, judge_comments, application_links, documents, photos) 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, awardData.description || null, awardData.judge_comments || null, awardData.application_links ? JSON.stringify(awardData.application_links) : null, awardData.documents ? JSON.stringify(awardData.documents) : null, awardData.photos ? JSON.stringify(awardData.photos) : null ]; const result = await DatabaseServiceBase.safeInsert(sql, params); // 獲取創建的獎項(使用最後創建的獎項) const createdAward = await this.getAwardByCompetitionAndCreator(awardData.competition_id, awardData.creator); return createdAward 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 getAllAwards(): Promise { try { console.log('🔍 開始查詢所有獎項...'); // 先測試簡單查詢 const simpleSql = 'SELECT * FROM awards ORDER BY created_at DESC LIMIT 5'; console.log('📝 執行簡單查詢:', simpleSql); const simpleResult = await db.query(simpleSql); console.log('✅ 簡單查詢結果:', simpleResult?.length || 0, '個獎項'); // 再執行 JOIN 查詢,包含團隊資訊 const sql = ` SELECT a.*, c.name as competition_name, c.type as competition_type, c.description as competition_description, c.start_date as competition_start_date, c.end_date as competition_end_date, t.name as team_name_from_teams FROM awards a LEFT JOIN competitions c ON a.competition_id = c.id LEFT JOIN teams t ON a.team_id = t.id ORDER BY a.created_at DESC `; console.log('📝 執行JOIN查詢:', sql); const result = await db.query(sql); console.log('✅ JOIN查詢結果:', result?.length || 0, '個獎項'); // 檢查第一個獎項的數據 if (result && result.length > 0) { console.log('🔍 第一個獎項數據:', { id: result[0].id, competition_name: (result[0] as any).competition_name, competition_type: (result[0] as any).competition_type, competition_id: result[0].competition_id, team_name_from_teams: (result[0] as any).team_name_from_teams, team_name: (result[0] as any).team_name, app_name: (result[0] as any).app_name, team_id: (result[0] as any).team_id }); } return result; } catch (error) { console.error('❌ 查詢獎項失敗:', error); throw error; } } // 根據競賽獲取獎項 static async getAwardsByCompetition(competitionId: string): Promise { const sql = ` SELECT a.*, c.name as competition_name, c.type as competition_type, c.description as competition_description, c.start_date as competition_start_date, c.end_date as competition_end_date, t.name as team_name_from_teams FROM awards a LEFT JOIN competitions c ON a.competition_id = c.id LEFT JOIN teams t ON a.team_id = t.id WHERE a.competition_id = ? ORDER BY a.created_at DESC `; return await db.query(sql, [competitionId]); } // 根據ID獲取獎項 static async getAwardById(id: string): Promise { const sql = 'SELECT * FROM awards WHERE id = ?'; return await db.queryOne(sql, [id]); } // 更新獎項 static async updateAward(id: string, updates: Partial): Promise { const fields = Object.keys(updates).filter(key => key !== 'id' && key !== 'created_at' ); if (fields.length === 0) return true; const setClause = fields.map(field => `${field} = ?`).join(', '); const values = fields.map(field => (updates as any)[field]); const sql = `UPDATE awards SET ${setClause} WHERE id = ?`; const result = await DatabaseServiceBase.safeUpdate(sql, [...values, id]); return result.affectedRows > 0; } // 刪除獎項 static async deleteAward(id: string): Promise { const sql = 'DELETE FROM awards WHERE id = ?'; const result = await DatabaseServiceBase.safeDelete(sql, [id]); return result.affectedRows > 0; } // 獲取獎項統計 static async getAwardStats(competitionId?: string): Promise<{ total: number; byType: Record; byCategory: Record; byYear: Record; }> { let sql = 'SELECT award_type, category, year FROM awards'; const params: any[] = []; if (competitionId) { sql += ' WHERE competition_id = ?'; params.push(competitionId); } const awards = await DatabaseServiceBase.safeQuery(sql, params); const stats = { total: awards.length, byType: {} as Record, byCategory: {} as Record, byYear: {} as Record, }; awards.forEach((award: any) => { // 統計獎項類型 stats.byType[award.award_type] = (stats.byType[award.award_type] || 0) + 1; // 統計獎項類別 stats.byCategory[award.category] = (stats.byCategory[award.category] || 0) + 1; // 統計年份 stats.byYear[award.year] = (stats.byYear[award.year] || 0) + 1; }); return stats; } // 根據年份獲取獎項 static async getAwardsByYear(year: number): Promise { const sql = ` SELECT a.*, c.name as competition_name, c.type as competition_type, c.description as competition_description, c.start_date as competition_start_date, c.end_date as competition_end_date, t.name as team_name_from_teams FROM awards a LEFT JOIN competitions c ON a.competition_id = c.id LEFT JOIN teams t ON a.team_id = t.id WHERE a.year = ? ORDER BY a.month DESC, a.rank ASC `; return await db.query(sql, [year]); } } // ===================================================== // 系統設定服務 // ===================================================== export class SystemSettingService extends DatabaseServiceBase { // 獲取設定值 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 DatabaseServiceBase.safeInsert(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); } }