Files
ai-showcase-platform/lib/services/database-service.ts

4759 lines
156 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// =====================================================
// 資料庫服務層
// =====================================================
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<User, 'id' | 'created_at' | 'updated_at'> & { id: string }): Promise<User> {
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<User | null> {
const sql = 'SELECT * FROM users WHERE email = ? AND status = "active"';
return await this.safeQueryOne<User>(sql, [email]);
}
// 根據ID獲取用戶
async findById(id: string): Promise<User | null> {
const sql = 'SELECT * FROM users WHERE id = ? AND status = "active"';
return await this.safeQueryOne<User>(sql, [id]);
}
// 更新用戶
async update(id: string, updates: Partial<User>): Promise<User | null> {
const fields = Object.keys(updates).filter(key => key !== 'id' && key !== 'created_at');
const setClause = fields.map(field => `${field} = ?`).join(', ');
const values = fields.map(field => (updates as any)[field]);
const sql = `UPDATE users SET ${setClause}, updated_at = CURRENT_TIMESTAMP WHERE id = ?`;
const result = await DatabaseServiceBase.safeUpdate(sql, [...values, id]);
if (result.affectedRows > 0) {
return await this.findById(id);
}
return null;
}
// 更新最後登入時間
async updateLastLogin(id: string): Promise<boolean> {
const sql = 'UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?';
const result = await DatabaseServiceBase.safeUpdate(sql, [id]);
return result.affectedRows > 0;
}
// 獲取用戶統計
async getUserStatistics(id: string): Promise<UserStatistics | null> {
const sql = 'SELECT * FROM user_statistics WHERE id = ?';
return await db.queryOne<UserStatistics>(sql, [id]);
}
// 獲取所有用戶(管理員用)
async findAll(filters: {
search?: string;
department?: string;
role?: string;
status?: string;
page?: number;
limit?: number;
} = {}): Promise<{ users: User[]; total: number }> {
const { search, department, role, status, page = 1, limit = 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<User>(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<any> {
// 獲取創建應用數量
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<any | null> {
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<T = any>(sql: string, params: any[] = []): Promise<T[]> {
return await this.safeQuery<T>(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<any[]> {
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<any[]> {
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<T = any>(sql: string, params: any[] = []): Promise<T | null> {
return await this.safeQueryOne<T>(sql, params);
}
// 獲取所有用戶
async getAllUsers(limit = 50, offset = 0): Promise<User[]> {
const sql = 'SELECT * FROM users ORDER BY created_at DESC LIMIT ? OFFSET ?';
return await this.safeQuery<User>(sql, [limit, offset]);
}
// 靜態方法保持向後兼容
static async createUser(userData: Omit<User, 'id' | 'created_at' | 'updated_at'> & { id?: string }): Promise<User> {
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<User, 'id' | 'created_at' | 'updated_at'> & { id: string });
}
static async getUserByEmail(email: string): Promise<User | null> {
const service = new UserService();
return await service.findByEmail(email);
}
static async getUserById(id: string): Promise<User | null> {
const service = new UserService();
return await service.findById(id);
}
static async updateUser(id: string, updates: Partial<User>): Promise<boolean> {
const service = new UserService();
const result = await service.update(id, updates);
return result !== null;
}
static async getUserStatistics(id: string): Promise<UserStatistics | null> {
const service = new UserService();
return await service.getUserStatistics(id);
}
static async getAllUsers(limit = 50, offset = 0): Promise<User[]> {
const service = new UserService();
return await service.getAllUsers(limit, offset);
}
// 獲取用戶的應用和評價統計
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<Judge, 'id' | 'created_at' | 'updated_at'>): Promise<Judge> {
const sql = `
INSERT INTO judges (id, name, title, department, expertise, avatar, is_active)
VALUES (UUID(), ?, ?, ?, ?, ?, ?)
`;
const params = [
judgeData.name,
judgeData.title,
judgeData.department,
JSON.stringify(judgeData.expertise),
judgeData.avatar || null,
judgeData.is_active
];
await DatabaseServiceBase.safeInsert(sql, params);
return await JudgeService.getJudgeByName(judgeData.name) as Judge;
}
// 根據姓名獲取評審
static async getJudgeByName(name: string): Promise<Judge | null> {
const sql = 'SELECT * FROM judges WHERE name = ?';
const result = await db.queryOne<Judge>(sql, [name]);
if (result) {
result.expertise = this.parseExpertise(result.expertise as any);
}
return result;
}
// 根據ID獲取評審
static async getJudgeById(id: string): Promise<Judge | null> {
const sql = 'SELECT * FROM judges WHERE id = ?';
const result = await db.queryOne<Judge>(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<Judge[]> {
const sql = 'SELECT * FROM judges ORDER BY created_at DESC';
const results = await db.query<Judge>(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<Judge>): Promise<boolean> {
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<boolean> {
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<any[]> {
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<string> {
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<any[]> {
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<any | null> {
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<any | null> {
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<boolean> {
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<boolean> {
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<boolean> {
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<boolean> {
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<any[]> {
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<boolean> {
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<boolean> {
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<boolean> {
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<boolean> {
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<any[]> {
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<any> {
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<Competition, 'id' | 'created_at' | 'updated_at'>): Promise<Competition> {
// 使用智能雙寫,每個資料庫生成自己的 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<Competition | null> {
const sql = 'SELECT * FROM competitions WHERE id = ?';
return await db.queryOne<Competition>(sql, [id]);
}
// 根據名稱獲取競賽
static async getCompetitionByName(name: string): Promise<Competition | null> {
const sql = 'SELECT * FROM competitions WHERE name = ? AND is_active = 1';
return await db.queryOne<Competition>(sql, [name]);
}
// 根據名稱獲取備機競賽 ID
static async getSlaveCompetitionIdByName(name: string): Promise<string | null> {
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<Competition[]> {
const sql = 'SELECT * FROM competitions WHERE is_active = 1 ORDER BY year DESC, month DESC';
const competitions = await db.query<any>(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<CompetitionStatistics | null> {
const sql = 'SELECT * FROM competition_statistics WHERE id = ?';
return await db.queryOne<CompetitionStatistics>(sql, [id]);
}
// 更新競賽
static async updateCompetition(id: string, updates: Partial<Competition>): Promise<boolean> {
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<any | null> {
try {
const sql = 'SELECT * FROM competitions WHERE is_current = 1 AND is_active = 1 LIMIT 1';
const competitions = await db.query<any>(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<boolean> {
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<boolean> {
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<boolean> {
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<void> {
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<boolean> {
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<boolean> {
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<any[]> {
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<any[]> {
// 先獲取競賽信息
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<boolean> {
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<boolean> {
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<boolean> {
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<boolean> {
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<any[]> {
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<boolean> {
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<boolean> {
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<boolean> {
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<boolean> {
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<any> {
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<any[]> {
try {
console.log('🔍 查詢競賽評審:', competitionId);
// 先檢查 competition_judges 表是否有資料
const checkSql = `SELECT COUNT(*) as count FROM competition_judges WHERE competition_id = ?`;
const checkResult = await db.query<any>(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<any>(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<any>(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<any>(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<any | null> {
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<any | null> {
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<any | null> {
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<T = any>(sql: string, params: any[] = []): Promise<T[]> {
return await this.safeQuery<T>(sql, params);
}
// 通用單一查詢方法 - 使用安全查詢確保連線釋放
async queryOne<T = any>(sql: string, params: any[] = []): Promise<T | null> {
return await this.safeQueryOne<T>(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<any[]> {
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<number> {
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<string | null> {
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<string | null> {
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<any> {
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<boolean> {
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<boolean> {
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<boolean> {
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<boolean> {
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<boolean> {
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<number> {
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<boolean> {
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<string[]> {
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<string[]> {
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<boolean> {
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<any[]> {
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<any> {
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<any[]> {
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<string> } } = {};
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<string> {
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<any[]> {
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<string | null> {
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<any> {
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<any> {
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<AppJudgeScore | null> {
const sql = 'SELECT * FROM app_judge_scores WHERE judge_id = ? AND app_id = ?';
return await db.queryOne<AppJudgeScore>(sql, [judgeId, appId]);
}
// 獲取應用的所有評分
static async getAppScores(appId: string): Promise<AppJudgeScore[]> {
const sql = 'SELECT * FROM app_judge_scores WHERE app_id = ? ORDER BY submitted_at DESC';
return await db.query<AppJudgeScore>(sql, [appId]);
}
// 提交提案評分
static async submitProposalScore(scoreData: Omit<ProposalJudgeScore, 'id' | 'submitted_at'>): Promise<ProposalJudgeScore> {
const sql = `
INSERT INTO proposal_judge_scores (id, judge_id, proposal_id, problem_identification_score, solution_feasibility_score, innovation_score, impact_score, presentation_score, total_score, comments)
VALUES (UUID(), ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
problem_identification_score = VALUES(problem_identification_score),
solution_feasibility_score = VALUES(solution_feasibility_score),
innovation_score = VALUES(innovation_score),
impact_score = VALUES(impact_score),
presentation_score = VALUES(presentation_score),
total_score = VALUES(total_score),
comments = VALUES(comments),
submitted_at = CURRENT_TIMESTAMP
`;
const params = [
scoreData.judge_id,
scoreData.proposal_id,
scoreData.problem_identification_score,
scoreData.solution_feasibility_score,
scoreData.innovation_score,
scoreData.impact_score,
scoreData.presentation_score,
scoreData.total_score,
scoreData.comments || null
];
await DatabaseServiceBase.safeInsert(sql, params);
return await this.getProposalScore(scoreData.judge_id, scoreData.proposal_id) as ProposalJudgeScore;
}
// 獲取提案評分
static async getProposalScore(judgeId: string, proposalId: string): Promise<ProposalJudgeScore | null> {
const sql = 'SELECT * FROM proposal_judge_scores WHERE judge_id = ? AND proposal_id = ?';
return await db.queryOne<ProposalJudgeScore>(sql, [judgeId, proposalId]);
}
// 獲取提案的所有評分
static async getProposalScores(proposalId: string): Promise<ProposalJudgeScore[]> {
const sql = 'SELECT * FROM proposal_judge_scores WHERE proposal_id = ? ORDER BY submitted_at DESC';
return await db.query<ProposalJudgeScore>(sql, [proposalId]);
}
// 獲取競賽的所有評分記錄(包含評審和參賽者信息)
static async getCompetitionScores(competitionId: string): Promise<any[]> {
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<AppJudgeScore, 'id' | 'submitted_at'> & { teamId: string }): Promise<AppJudgeScore> {
// 創建一個虛擬的應用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<any[]> {
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<boolean> {
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<AppJudgeScore>): Promise<boolean> {
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<ProposalJudgeScore>): Promise<boolean> {
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<Award> {
const sql = `
INSERT INTO awards (id, competition_id, app_id, team_id, proposal_id, app_name, team_name, proposal_title, creator, award_type, award_name, score, year, month, icon, custom_award_type_id, competition_type, \`rank\`, category, 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<Award | null> {
const sql = 'SELECT * FROM awards WHERE competition_id = ? AND creator = ? ORDER BY created_at DESC LIMIT 1';
return await db.queryOne<Award>(sql, [competitionId, creator]);
}
// 獲取所有獎項
static async getAllAwards(): Promise<Award[]> {
try {
console.log('🔍 開始查詢所有獎項...');
// 先測試簡單查詢
const simpleSql = 'SELECT * FROM awards ORDER BY created_at DESC LIMIT 5';
console.log('📝 執行簡單查詢:', simpleSql);
const simpleResult = await db.query<Award>(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<Award>(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<Award[]> {
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<Award>(sql, [competitionId]);
}
// 根據ID獲取獎項
static async getAwardById(id: string): Promise<Award | null> {
const sql = 'SELECT * FROM awards WHERE id = ?';
return await db.queryOne<Award>(sql, [id]);
}
// 更新獎項
static async updateAward(id: string, updates: Partial<Award>): Promise<boolean> {
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<boolean> {
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<string, number>;
byCategory: Record<string, number>;
byYear: Record<number, number>;
}> {
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<string, number>,
byCategory: {} as Record<string, number>,
byYear: {} as Record<number, number>,
};
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<Award[]> {
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<Award>(sql, [year]);
}
}
// =====================================================
// 系統設定服務
// =====================================================
export class SystemSettingService extends DatabaseServiceBase {
// 獲取設定值
static async getSetting(key: string): Promise<string | null> {
const sql = 'SELECT value FROM system_settings WHERE `key` = ?';
const result = await db.queryOne<{ value: string }>(sql, [key]);
return result?.value || null;
}
// 設定值
static async setSetting(key: string, value: string, description?: string, category = 'general', isPublic = false): Promise<boolean> {
const sql = `
INSERT INTO system_settings (id, \`key\`, value, description, category, is_public)
VALUES (UUID(), ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
value = VALUES(value),
description = VALUES(description),
category = VALUES(category),
is_public = VALUES(is_public),
updated_at = CURRENT_TIMESTAMP
`;
const params = [key, value, description || null, category, isPublic];
const result = await DatabaseServiceBase.safeInsert(sql, params);
return result.affectedRows > 0;
}
// 獲取所有公開設定
static async getPublicSettings(): Promise<SystemSetting[]> {
const sql = 'SELECT * FROM system_settings WHERE is_public = TRUE ORDER BY category, `key`';
return await db.query<SystemSetting>(sql);
}
}