3598 lines
114 KiB
TypeScript
3598 lines
114 KiB
TypeScript
// =====================================================
|
||
// 資料庫服務層
|
||
// =====================================================
|
||
|
||
import { db } from '../database';
|
||
import { dbSync } from '../database-sync';
|
||
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 {
|
||
// 創建用戶
|
||
async create(userData: Omit<User, 'id' | 'created_at' | 'updated_at'>): Promise<User> {
|
||
const sql = `
|
||
INSERT INTO users (id, name, email, password_hash, avatar, department, role, join_date, total_likes, total_views, status, last_login)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||
`;
|
||
const params = [
|
||
userData.id,
|
||
userData.name,
|
||
userData.email,
|
||
userData.password_hash,
|
||
userData.avatar || null,
|
||
userData.department,
|
||
userData.role,
|
||
userData.join_date,
|
||
userData.total_likes,
|
||
userData.total_views,
|
||
userData.status,
|
||
userData.last_login || null
|
||
];
|
||
|
||
await db.insert(sql, params);
|
||
return await this.findByEmail(userData.email) as User;
|
||
}
|
||
|
||
// 根據郵箱獲取用戶
|
||
async findByEmail(email: string): Promise<User | null> {
|
||
const sql = 'SELECT * FROM users WHERE email = ? AND status = "active"';
|
||
return await db.queryOne<User>(sql, [email]);
|
||
}
|
||
|
||
// 根據ID獲取用戶
|
||
async findById(id: string): Promise<User | null> {
|
||
const sql = 'SELECT * FROM users WHERE id = ? AND status = "active"';
|
||
return await db.queryOne<User>(sql, [id]);
|
||
}
|
||
|
||
// 更新用戶
|
||
async update(id: string, updates: Partial<User>): Promise<User | null> {
|
||
const fields = Object.keys(updates).filter(key => key !== 'id' && key !== 'created_at');
|
||
const setClause = fields.map(field => `${field} = ?`).join(', ');
|
||
const values = fields.map(field => (updates as any)[field]);
|
||
|
||
const sql = `UPDATE users SET ${setClause}, updated_at = CURRENT_TIMESTAMP WHERE id = ?`;
|
||
const result = await db.update(sql, [...values, id]);
|
||
|
||
if (result.affectedRows > 0) {
|
||
return await this.findById(id);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
// 更新最後登入時間
|
||
async updateLastLogin(id: string): Promise<boolean> {
|
||
const sql = 'UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?';
|
||
const result = await db.update(sql, [id]);
|
||
return result.affectedRows > 0;
|
||
}
|
||
|
||
// 獲取用戶統計
|
||
async getUserStatistics(id: string): Promise<UserStatistics | null> {
|
||
const sql = 'SELECT * FROM user_statistics WHERE id = ?';
|
||
return await db.queryOne<UserStatistics>(sql, [id]);
|
||
}
|
||
|
||
// 獲取所有用戶(管理員用)
|
||
async findAll(filters: {
|
||
search?: string;
|
||
department?: string;
|
||
role?: string;
|
||
status?: string;
|
||
page?: number;
|
||
limit?: number;
|
||
} = {}): Promise<{ users: User[]; total: number }> {
|
||
const { search, department, role, status, page = 1, limit = 10 } = filters;
|
||
|
||
// 構建查詢條件
|
||
let whereConditions: string[] = [];
|
||
let params: any[] = [];
|
||
|
||
if (search) {
|
||
whereConditions.push('(name LIKE ? OR email LIKE ?)');
|
||
params.push(`%${search}%`, `%${search}%`);
|
||
}
|
||
|
||
if (department && department !== 'all') {
|
||
whereConditions.push('department = ?');
|
||
params.push(department);
|
||
}
|
||
|
||
if (role && role !== 'all') {
|
||
whereConditions.push('role = ?');
|
||
params.push(role);
|
||
}
|
||
|
||
if (status && status !== 'all') {
|
||
if (status === 'active') {
|
||
whereConditions.push('status = ? AND last_login IS NOT NULL AND last_login >= DATE_SUB(NOW(), INTERVAL 30 DAY)');
|
||
params.push('active');
|
||
} else if (status === 'inactive') {
|
||
whereConditions.push('status = ?');
|
||
params.push('inactive');
|
||
} else if (status === 'invited') {
|
||
whereConditions.push('status = ?');
|
||
params.push('invited');
|
||
}
|
||
}
|
||
|
||
const whereClause = whereConditions.length > 0 ? `WHERE ${whereConditions.join(' AND ')}` : '';
|
||
|
||
// 獲取總數
|
||
const countSql = `SELECT COUNT(*) as total FROM users ${whereClause}`;
|
||
const countResult = await this.query(countSql, params);
|
||
const total = countResult[0]?.total || 0;
|
||
|
||
// 獲取用戶列表
|
||
const offset = (page - 1) * limit;
|
||
const usersSql = `
|
||
SELECT
|
||
id, name, email, avatar, department, role, join_date,
|
||
total_likes, total_views, status, last_login, created_at, updated_at
|
||
FROM users
|
||
${whereClause}
|
||
ORDER BY created_at DESC
|
||
LIMIT ${offset}, ${limit}
|
||
`;
|
||
const users = await this.query<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 this.query(sql);
|
||
const stats = result[0] || {};
|
||
|
||
return {
|
||
totalUsers: stats.total_users || 0,
|
||
activeUsers: stats.active_users || 0,
|
||
adminCount: stats.admin_count || 0,
|
||
developerCount: stats.developer_count || 0,
|
||
invitedUsers: stats.invited_users || 0,
|
||
inactiveUsers: stats.inactive_users || 0,
|
||
newThisMonth: stats.new_this_month || 0
|
||
};
|
||
}
|
||
|
||
// 獲取用戶活動記錄
|
||
async getUserActivities(
|
||
userId: string,
|
||
page: number = 1,
|
||
limit: number = 10,
|
||
filters: {
|
||
search?: string;
|
||
startDate?: string;
|
||
endDate?: string;
|
||
activityType?: string;
|
||
} = {}
|
||
): Promise<{ activities: any[]; pagination: any }> {
|
||
// 構建篩選條件
|
||
let whereConditions = [];
|
||
let dateFilter = '';
|
||
|
||
// 日期篩選
|
||
if (filters.startDate && filters.endDate) {
|
||
dateFilter = `AND created_at BETWEEN '${filters.startDate} 00:00:00' AND '${filters.endDate} 23:59:59'`;
|
||
} else if (filters.startDate) {
|
||
dateFilter = `AND created_at >= '${filters.startDate} 00:00:00'`;
|
||
} else if (filters.endDate) {
|
||
dateFilter = `AND created_at <= '${filters.endDate} 23:59:59'`;
|
||
}
|
||
|
||
// 活動類型篩選
|
||
let activityTypeFilter = '';
|
||
if (filters.activityType && filters.activityType !== 'all') {
|
||
activityTypeFilter = `AND activity_type = '${filters.activityType}'`;
|
||
}
|
||
|
||
// 搜尋篩選
|
||
let searchFilter = '';
|
||
if (filters.search) {
|
||
searchFilter = `AND action_text LIKE '%${filters.search}%'`;
|
||
}
|
||
|
||
// 使用字符串插值避免 UNION ALL 參數化查詢問題
|
||
const baseSql = `
|
||
SELECT
|
||
'login' as activity_type,
|
||
'登入系統' as action_text,
|
||
'Calendar' as icon,
|
||
'blue' as color,
|
||
u.last_login as created_at,
|
||
NULL as app_name,
|
||
NULL as app_id
|
||
FROM users u
|
||
WHERE u.id = '${userId}' AND u.last_login IS NOT NULL
|
||
|
||
UNION ALL
|
||
|
||
SELECT
|
||
'favorite' as activity_type,
|
||
CONCAT('收藏應用:', a.name) as action_text,
|
||
'Heart' as icon,
|
||
'red' as color,
|
||
uf.created_at,
|
||
a.name as app_name,
|
||
a.id as app_id
|
||
FROM user_favorites uf
|
||
JOIN apps a ON uf.app_id = a.id
|
||
WHERE uf.user_id = '${userId}'
|
||
|
||
UNION ALL
|
||
|
||
SELECT
|
||
'like' as activity_type,
|
||
CONCAT('按讚應用:', a.name) as action_text,
|
||
'ThumbsUp' as icon,
|
||
'green' as color,
|
||
ul.liked_at as created_at,
|
||
a.name as app_name,
|
||
a.id as app_id
|
||
FROM user_likes ul
|
||
JOIN apps a ON ul.app_id = a.id
|
||
WHERE ul.user_id = '${userId}'
|
||
|
||
UNION ALL
|
||
|
||
SELECT
|
||
'view' as activity_type,
|
||
CONCAT('查看應用:', a.name) as action_text,
|
||
'Eye' as icon,
|
||
'purple' as color,
|
||
uv.viewed_at as created_at,
|
||
a.name as app_name,
|
||
a.id as app_id
|
||
FROM user_views uv
|
||
JOIN apps a ON uv.app_id = a.id
|
||
WHERE uv.user_id = '${userId}'
|
||
|
||
UNION ALL
|
||
|
||
SELECT
|
||
'rating' as activity_type,
|
||
CONCAT('評價應用:', a.name, ' (', ur.rating, '分)') as action_text,
|
||
'Star' as icon,
|
||
'yellow' as color,
|
||
ur.rated_at as created_at,
|
||
a.name as app_name,
|
||
a.id as app_id
|
||
FROM user_ratings ur
|
||
JOIN apps a ON ur.app_id = a.id
|
||
WHERE ur.user_id = '${userId}'
|
||
|
||
UNION ALL
|
||
|
||
SELECT
|
||
'create' as activity_type,
|
||
CONCAT('創建應用:', a.name) as action_text,
|
||
'Plus' as icon,
|
||
'blue' as color,
|
||
a.created_at,
|
||
a.name as app_name,
|
||
a.id as app_id
|
||
FROM apps a
|
||
WHERE a.creator_id = '${userId}'
|
||
`;
|
||
|
||
// 先獲取總數
|
||
const countSql = `
|
||
SELECT COUNT(*) as total FROM (
|
||
${baseSql}
|
||
) as activities
|
||
WHERE 1=1 ${dateFilter} ${activityTypeFilter} ${searchFilter}
|
||
`;
|
||
|
||
const countResult = await this.query(countSql, []);
|
||
const total = countResult[0]?.total || 0;
|
||
const totalPages = Math.ceil(total / limit);
|
||
const offset = (page - 1) * limit;
|
||
|
||
// 獲取分頁數據
|
||
const dataSql = `
|
||
SELECT * FROM (
|
||
${baseSql}
|
||
) as activities
|
||
WHERE 1=1 ${dateFilter} ${activityTypeFilter} ${searchFilter}
|
||
ORDER BY created_at DESC
|
||
LIMIT ${limit} OFFSET ${offset}
|
||
`;
|
||
|
||
const activities = await this.query(dataSql, []);
|
||
|
||
return {
|
||
activities,
|
||
pagination: {
|
||
page,
|
||
limit,
|
||
total,
|
||
totalPages
|
||
}
|
||
};
|
||
}
|
||
|
||
// 獲取用戶詳細統計數據
|
||
async getUserDetailedStats(userId: string): Promise<any> {
|
||
// 獲取創建應用數量
|
||
const appsSql = `
|
||
SELECT COUNT(*) as total_apps
|
||
FROM apps
|
||
WHERE creator_id = '${userId}' AND is_active = TRUE
|
||
`;
|
||
const appsResult = await this.query(appsSql, []);
|
||
const totalApps = appsResult[0]?.total_apps || 0;
|
||
|
||
// 獲取撰寫評價數量
|
||
const reviewsSql = `
|
||
SELECT COUNT(*) as total_reviews
|
||
FROM user_ratings
|
||
WHERE user_id = '${userId}'
|
||
`;
|
||
const reviewsResult = await this.query(reviewsSql, []);
|
||
const totalReviews = reviewsResult[0]?.total_reviews || 0;
|
||
|
||
// 獲取獲得讚數(用戶創建的應用獲得的總讚數)
|
||
const likesSql = `
|
||
SELECT COALESCE(SUM(a.likes_count), 0) as total_likes
|
||
FROM apps a
|
||
WHERE a.creator_id = '${userId}' AND a.is_active = TRUE
|
||
`;
|
||
const likesResult = await this.query(likesSql, []);
|
||
const totalLikes = likesResult[0]?.total_likes || 0;
|
||
|
||
// 獲取登入天數(從註冊日期到現在的天數)
|
||
const loginDaysSql = `
|
||
SELECT DATEDIFF(CURDATE(), join_date) as login_days
|
||
FROM users
|
||
WHERE id = '${userId}'
|
||
`;
|
||
const loginDaysResult = await this.query(loginDaysSql, []);
|
||
const loginDays = loginDaysResult[0]?.login_days || 0;
|
||
|
||
return {
|
||
totalApps,
|
||
totalReviews,
|
||
totalLikes,
|
||
loginDays
|
||
};
|
||
}
|
||
|
||
// 更新用戶資料
|
||
async updateUser(userId: string, userData: {
|
||
name: string;
|
||
department: string;
|
||
role: string;
|
||
status: string;
|
||
}): Promise<{ success: boolean; user?: any; error?: string }> {
|
||
try {
|
||
const sql = `
|
||
UPDATE users
|
||
SET name = ?, department = ?, role = ?, status = ?, updated_at = NOW()
|
||
WHERE id = ?
|
||
`;
|
||
|
||
await this.query(sql, [
|
||
userData.name,
|
||
userData.department,
|
||
userData.role,
|
||
userData.status,
|
||
userId
|
||
]);
|
||
|
||
// 獲取更新後的用戶資料
|
||
const updatedUserSql = `
|
||
SELECT id, name, email, department, role, status, join_date, last_login, total_likes, total_views, created_at, updated_at
|
||
FROM users
|
||
WHERE id = ?
|
||
`;
|
||
|
||
const updatedUser = await this.queryOne(updatedUserSql, [userId]);
|
||
|
||
if (!updatedUser) {
|
||
return { success: false, error: '用戶不存在' };
|
||
}
|
||
|
||
return { success: true, user: updatedUser };
|
||
} catch (error) {
|
||
console.error('更新用戶錯誤:', error);
|
||
return { success: false, error: '更新用戶時發生錯誤' };
|
||
}
|
||
}
|
||
|
||
// 刪除用戶
|
||
async deleteUser(userId: string): Promise<{ success: boolean; error?: string }> {
|
||
try {
|
||
// 檢查用戶是否存在
|
||
const checkUserSql = `SELECT id FROM users WHERE id = ?`;
|
||
const user = await this.queryOne(checkUserSql, [userId]);
|
||
|
||
if (!user) {
|
||
return { success: false, error: '用戶不存在' };
|
||
}
|
||
|
||
// 刪除用戶(由於外鍵約束,相關的活動記錄也會被自動刪除)
|
||
const deleteSql = `DELETE FROM users WHERE id = ?`;
|
||
await this.query(deleteSql, [userId]);
|
||
|
||
return { success: true };
|
||
} catch (error) {
|
||
console.error('刪除用戶錯誤:', error);
|
||
return { success: false, error: '刪除用戶時發生錯誤' };
|
||
}
|
||
}
|
||
|
||
// 創建邀請用戶
|
||
async createInvitedUser(email: string, role: string): Promise<{ success: boolean; user?: any; invitationLink?: string; error?: string }> {
|
||
try {
|
||
// 生成邀請 token
|
||
const invitationToken = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15)
|
||
|
||
// 創建邀請用戶記錄
|
||
const userId = crypto.randomUUID()
|
||
const sql = `
|
||
INSERT INTO users (id, name, email, password_hash, department, role, status, join_date, created_at, updated_at)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, CURDATE(), NOW(), NOW())
|
||
`;
|
||
|
||
// 為邀請用戶創建一個臨時密碼(用戶註冊時會覆蓋)
|
||
const tempPassword = 'temp_' + Math.random().toString(36).substring(2, 15)
|
||
const tempPasswordHash = await bcrypt.hash(tempPassword, 10)
|
||
|
||
await this.query(sql, [
|
||
userId,
|
||
'', // 姓名留空,用戶註冊時填寫
|
||
email,
|
||
tempPasswordHash,
|
||
'', // 部門留空,用戶註冊時填寫
|
||
role,
|
||
'invited'
|
||
]);
|
||
|
||
// 獲取創建的用戶資料
|
||
const userSql = `
|
||
SELECT id, name, email, department, role, status, join_date, created_at, updated_at
|
||
FROM users
|
||
WHERE id = ?
|
||
`;
|
||
|
||
const user = await this.queryOne(userSql, [userId])
|
||
|
||
if (!user) {
|
||
return { success: false, error: '創建邀請用戶失敗' }
|
||
}
|
||
|
||
// 生成邀請連結
|
||
const invitationLink = `${process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000'}/register?token=${invitationToken}&email=${encodeURIComponent(email)}&role=${role}`
|
||
|
||
return {
|
||
success: true,
|
||
user: {
|
||
...user,
|
||
invitationToken,
|
||
invitationLink
|
||
},
|
||
invitationLink
|
||
}
|
||
} catch (error) {
|
||
console.error('創建邀請用戶錯誤:', error);
|
||
return { success: false, error: '創建邀請用戶時發生錯誤' };
|
||
}
|
||
}
|
||
|
||
// 查找邀請用戶(狀態為 invited)
|
||
async findInvitedUserByEmail(email: string): Promise<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 db.query<T>(sql, params);
|
||
}
|
||
|
||
// 獲取儀表板統計數據
|
||
async getDashboardStats(): Promise<{
|
||
totalUsers: number;
|
||
activeUsers: number;
|
||
totalApps: number;
|
||
totalCompetitions: number;
|
||
totalReviews: number;
|
||
totalViews: number;
|
||
totalLikes: number;
|
||
newAppsThisMonth: number;
|
||
activeCompetitions: number;
|
||
growthRate: number;
|
||
}> {
|
||
try {
|
||
// 用戶統計
|
||
const userStats = await this.getUserStats();
|
||
|
||
// 應用統計
|
||
const appStatsSql = `
|
||
SELECT
|
||
COUNT(*) as total_apps,
|
||
COUNT(CASE WHEN created_at >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) THEN 1 END) as new_apps_this_month,
|
||
COALESCE(SUM(views_count), 0) as total_views,
|
||
COALESCE(SUM(likes_count), 0) as total_likes
|
||
FROM apps
|
||
WHERE is_active = TRUE
|
||
`;
|
||
const appStats = await this.queryOne(appStatsSql);
|
||
|
||
// 評論統計
|
||
const reviewStatsSql = `
|
||
SELECT COUNT(*) as total_reviews
|
||
FROM user_ratings
|
||
`;
|
||
const reviewStats = await this.queryOne(reviewStatsSql);
|
||
|
||
// 競賽統計
|
||
const competitionStatsSql = `
|
||
SELECT
|
||
COUNT(*) as total_competitions,
|
||
COUNT(CASE WHEN status = 'active' OR status = 'ongoing' THEN 1 END) as active_competitions
|
||
FROM competitions
|
||
`;
|
||
const competitionStats = await this.queryOne(competitionStatsSql);
|
||
|
||
// 計算增長率(與上個月比較)
|
||
const lastMonthUsersSql = `
|
||
SELECT COUNT(*) as last_month_users
|
||
FROM users
|
||
WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL 60 DAY)
|
||
AND created_at < DATE_SUB(CURDATE(), INTERVAL 30 DAY)
|
||
`;
|
||
const lastMonthUsers = await this.queryOne(lastMonthUsersSql);
|
||
|
||
const currentMonthUsers = userStats.newThisMonth;
|
||
const growthRate = lastMonthUsers?.last_month_users > 0
|
||
? Math.round(((currentMonthUsers - lastMonthUsers.last_month_users) / lastMonthUsers.last_month_users) * 100)
|
||
: 0;
|
||
|
||
return {
|
||
totalUsers: userStats.totalUsers,
|
||
activeUsers: userStats.activeUsers,
|
||
totalApps: appStats?.total_apps || 0,
|
||
totalCompetitions: competitionStats?.total_competitions || 0,
|
||
totalReviews: reviewStats?.total_reviews || 0,
|
||
totalViews: appStats?.total_views || 0,
|
||
totalLikes: appStats?.total_likes || 0,
|
||
newAppsThisMonth: appStats?.new_apps_this_month || 0,
|
||
activeCompetitions: competitionStats?.active_competitions || 0,
|
||
growthRate: growthRate
|
||
};
|
||
} catch (error) {
|
||
console.error('獲取儀表板統計數據錯誤:', error);
|
||
return {
|
||
totalUsers: 0,
|
||
activeUsers: 0,
|
||
totalApps: 0,
|
||
totalCompetitions: 0,
|
||
totalReviews: 0,
|
||
totalViews: 0,
|
||
totalLikes: 0,
|
||
newAppsThisMonth: 0,
|
||
activeCompetitions: 0,
|
||
growthRate: 0
|
||
};
|
||
}
|
||
}
|
||
|
||
// 獲取最新活動
|
||
async getRecentActivities(limit: number = 10): Promise<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 = TRUE
|
||
GROUP BY a.id, a.name, a.description, a.views_count, a.likes_count, a.category, a.created_at
|
||
ORDER BY (a.views_count + a.likes_count * 2) DESC
|
||
LIMIT ${limit}
|
||
`;
|
||
|
||
const apps = await this.query(sql);
|
||
|
||
return apps.map(app => ({
|
||
id: app.id,
|
||
name: app.name,
|
||
description: app.description,
|
||
views: app.views || 0,
|
||
likes: app.likes || 0,
|
||
rating: Math.round(app.rating * 10) / 10,
|
||
category: app.category,
|
||
created_at: app.created_at
|
||
}));
|
||
} catch (error) {
|
||
console.error('獲取熱門應用錯誤:', error);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
// 通用單一查詢方法
|
||
async queryOne<T = any>(sql: string, params: any[] = []): Promise<T | null> {
|
||
return await db.queryOne<T>(sql, params);
|
||
}
|
||
|
||
// 獲取所有用戶
|
||
async getAllUsers(limit = 50, offset = 0): Promise<User[]> {
|
||
const sql = 'SELECT * FROM users ORDER BY created_at DESC LIMIT ? OFFSET ?';
|
||
return await db.query<User>(sql, [limit, offset]);
|
||
}
|
||
|
||
// 靜態方法保持向後兼容
|
||
static async createUser(userData: Omit<User, 'id' | 'created_at' | 'updated_at'>): Promise<User> {
|
||
const service = new UserService();
|
||
return await service.create(userData);
|
||
}
|
||
|
||
static async getUserByEmail(email: string): Promise<User | null> {
|
||
const service = new UserService();
|
||
return await service.findByEmail(email);
|
||
}
|
||
|
||
static async getUserById(id: string): Promise<User | null> {
|
||
const service = new UserService();
|
||
return await service.findById(id);
|
||
}
|
||
|
||
static async updateUser(id: string, updates: Partial<User>): Promise<boolean> {
|
||
const service = new UserService();
|
||
const result = await service.update(id, updates);
|
||
return result !== null;
|
||
}
|
||
|
||
static async getUserStatistics(id: string): Promise<UserStatistics | null> {
|
||
const service = new UserService();
|
||
return await service.getUserStatistics(id);
|
||
}
|
||
|
||
static async getAllUsers(limit = 50, offset = 0): Promise<User[]> {
|
||
const service = new UserService();
|
||
return await service.getAllUsers(limit, offset);
|
||
}
|
||
|
||
// 獲取用戶的應用和評價統計
|
||
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 {
|
||
// 安全解析 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 [];
|
||
}
|
||
|
||
// 創建評審
|
||
static async createJudge(judgeData: Omit<Judge, 'id' | 'created_at' | 'updated_at'>): Promise<Judge> {
|
||
const sql = `
|
||
INSERT INTO judges (id, name, title, department, expertise, avatar, is_active)
|
||
VALUES (UUID(), ?, ?, ?, ?, ?, ?)
|
||
`;
|
||
const params = [
|
||
judgeData.name,
|
||
judgeData.title,
|
||
judgeData.department,
|
||
JSON.stringify(judgeData.expertise),
|
||
judgeData.avatar || null,
|
||
judgeData.is_active
|
||
];
|
||
|
||
await db.insert(sql, params);
|
||
return await this.getJudgeByName(judgeData.name) as Judge;
|
||
}
|
||
|
||
// 根據姓名獲取評審
|
||
static async getJudgeByName(name: string): Promise<Judge | null> {
|
||
const sql = 'SELECT * FROM judges WHERE name = ?';
|
||
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);
|
||
}
|
||
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)
|
||
}));
|
||
}
|
||
|
||
// 更新評審
|
||
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) {
|
||
console.log('沒有字段需要更新');
|
||
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 = ?`;
|
||
console.log('執行 SQL:', sql);
|
||
console.log('參數:', [...values, id]);
|
||
|
||
const result = await db.update(sql, [...values, id]);
|
||
console.log('更新結果:', result);
|
||
return result.affectedRows > 0;
|
||
}
|
||
|
||
// 刪除評審(硬刪除)
|
||
static async deleteJudge(id: string): Promise<boolean> {
|
||
try {
|
||
const sql = 'DELETE FROM judges WHERE id = ?';
|
||
const result = await db.delete(sql, [id]);
|
||
return result.affectedRows > 0;
|
||
} catch (error) {
|
||
console.error('刪除評審錯誤:', error);
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
|
||
// =====================================================
|
||
// 團隊服務
|
||
// =====================================================
|
||
export class TeamService {
|
||
// 創建團隊
|
||
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 db.insert(sql, params);
|
||
console.log('團隊創建結果:', result);
|
||
return id;
|
||
}
|
||
|
||
// 獲取所有團隊
|
||
static async getAllTeams(): Promise<any[]> {
|
||
const sql = `
|
||
SELECT t.*,
|
||
u.name as leader_name,
|
||
u.phone as leader_phone,
|
||
COUNT(tm.id) as member_count
|
||
FROM teams t
|
||
LEFT JOIN users u ON t.leader_id = u.id
|
||
LEFT JOIN team_members tm ON t.id = tm.team_id
|
||
WHERE t.is_active = TRUE
|
||
GROUP BY t.id
|
||
ORDER BY t.created_at DESC
|
||
`;
|
||
const results = await db.query(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 = TRUE
|
||
`;
|
||
const results = await db.query(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 = TRUE
|
||
`;
|
||
const results = await db.query(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;
|
||
}>): Promise<boolean> {
|
||
const fields = Object.keys(updates).filter(key => key !== 'id' && key !== 'created_at');
|
||
|
||
if (fields.length === 0) {
|
||
console.log('沒有字段需要更新');
|
||
return true;
|
||
}
|
||
|
||
const setClause = fields.map(field => `${field} = ?`).join(', ');
|
||
const values = fields.map(field => updates[field as keyof typeof updates]);
|
||
values.push(id);
|
||
|
||
const sql = `UPDATE teams SET ${setClause}, updated_at = CURRENT_TIMESTAMP WHERE id = ?`;
|
||
|
||
try {
|
||
const result = await db.update(sql, values);
|
||
console.log('團隊更新結果:', result);
|
||
return result.affectedRows > 0;
|
||
} 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 db.update(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 db.delete(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 db.insert(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 db.query(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 db.delete(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 db.update(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 = TRUE';
|
||
const result = await db.update(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 db.update(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 = TRUE
|
||
ORDER BY a.created_at DESC
|
||
`;
|
||
console.log('📝 getTeamApps SQL:', sql);
|
||
console.log('📝 getTeamApps 參數:', [teamId]);
|
||
const results = await db.query(sql, [teamId]);
|
||
console.log('📊 getTeamApps 結果:', results.length, '個應用');
|
||
return results;
|
||
}
|
||
|
||
// 獲取團隊統計
|
||
static async getTeamStats(): Promise<any> {
|
||
const sql = `
|
||
SELECT
|
||
COUNT(*) as totalTeams,
|
||
COUNT(CASE WHEN is_active = TRUE 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 db.query(sql);
|
||
return results[0] || { totalTeams: 0, activeTeams: 0, inactiveTeams: 0, avgMembersPerTeam: 0 };
|
||
}
|
||
}
|
||
|
||
// =====================================================
|
||
// 競賽服務
|
||
// =====================================================
|
||
export class CompetitionService {
|
||
// 創建競賽
|
||
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 = TRUE';
|
||
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 = TRUE 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 = TRUE AND is_active = TRUE 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 db.update('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 getCompetitionJudges(competitionId: string): Promise<any[]> {
|
||
const sql = `
|
||
SELECT j.*, cj.assigned_at
|
||
FROM competition_judges cj
|
||
JOIN judges j ON cj.judge_id = j.id
|
||
WHERE cj.competition_id = ? AND j.is_active = TRUE
|
||
ORDER BY cj.assigned_at ASC
|
||
`;
|
||
return await db.query(sql, [competitionId]);
|
||
}
|
||
|
||
// 為競賽添加評審
|
||
static async addCompetitionJudges(competitionId: string, judgeIds: string[]): Promise<boolean> {
|
||
try {
|
||
const dbSyncFixed = new DatabaseSyncFixed();
|
||
|
||
// 先刪除現有的評審關聯
|
||
await db.delete('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 db.delete(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
|
||
FROM competition_teams ct
|
||
JOIN teams t ON ct.team_id = t.id
|
||
LEFT JOIN users u ON t.leader_id = u.id
|
||
WHERE ct.competition_id = ? AND t.is_active = TRUE
|
||
ORDER BY ct.registered_at ASC
|
||
`;
|
||
return await db.query(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.team_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 = TRUE
|
||
ORDER BY a.created_at ASC
|
||
`;
|
||
apps = await db.query(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 = TRUE
|
||
ORDER BY ca.submitted_at ASC
|
||
`;
|
||
apps = await db.query(sql, [competitionId]);
|
||
}
|
||
|
||
return apps;
|
||
}
|
||
|
||
// 為競賽添加團隊
|
||
static async addCompetitionTeams(competitionId: string, teamIds: string[]): Promise<boolean> {
|
||
try {
|
||
const dbSyncFixed = new DatabaseSyncFixed();
|
||
|
||
// 先刪除現有的團隊關聯
|
||
await db.delete('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 db.delete(sql, [competitionId, teamId]);
|
||
return result.affectedRows > 0;
|
||
}
|
||
|
||
// 為競賽添加應用
|
||
static async addCompetitionApps(competitionId: string, appIds: string[]): Promise<boolean> {
|
||
try {
|
||
const dbSyncFixed = new DatabaseSyncFixed();
|
||
|
||
// 先刪除現有的應用關聯
|
||
await db.delete('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 db.delete(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 db.query(sql, [competitionId]);
|
||
}
|
||
|
||
// 為競賽添加獎項類型
|
||
static async addCompetitionAwardTypes(competitionId: string, awardTypes: any[]): Promise<boolean> {
|
||
try {
|
||
const dbSyncFixed = new DatabaseSyncFixed();
|
||
|
||
// 先刪除現有的獎項類型
|
||
await db.delete('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 db.delete(sql, [competitionId, awardTypeId]);
|
||
return result.affectedRows > 0;
|
||
}
|
||
|
||
// 獲取競賽的評分規則列表
|
||
static async getCompetitionRules(competitionId: string): Promise<any[]> {
|
||
const sql = `
|
||
SELECT cr.*
|
||
FROM competition_rules cr
|
||
WHERE cr.competition_id = ?
|
||
ORDER BY cr.order_index ASC, cr.created_at ASC
|
||
`;
|
||
return await db.query(sql, [competitionId]);
|
||
}
|
||
|
||
// 為競賽添加評分規則
|
||
static async addCompetitionRules(competitionId: string, rules: any[]): Promise<boolean> {
|
||
try {
|
||
const dbSyncFixed = new DatabaseSyncFixed();
|
||
|
||
// 先刪除現有的評分規則
|
||
await db.delete('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 db.delete(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),
|
||
this.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);
|
||
|
||
console.log('🔍 競賽狀態計算:', {
|
||
competitionId,
|
||
name: competition.name,
|
||
now: nowUTC.toISOString(),
|
||
startDate: startDateUTC.toISOString(),
|
||
endDate: endDateUTC.toISOString(),
|
||
originalStatus: competition.status
|
||
});
|
||
|
||
// 根據實際日期計算狀態
|
||
if (nowUTC < startDateUTC) {
|
||
calculatedStatus = 'upcoming'; // 即將開始
|
||
} else if (nowUTC >= startDateUTC && nowUTC <= endDateUTC) {
|
||
calculatedStatus = 'active'; // 進行中
|
||
} else if (nowUTC > endDateUTC) {
|
||
calculatedStatus = 'completed'; // 已完成
|
||
}
|
||
|
||
console.log('🔍 計算後的狀態:', calculatedStatus);
|
||
|
||
// 轉換字段名稱以匹配前端期望的格式
|
||
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
|
||
};
|
||
}
|
||
}
|
||
|
||
// =====================================================
|
||
// 應用服務
|
||
// =====================================================
|
||
export class AppService {
|
||
// 創建應用
|
||
async createApp(appData: {
|
||
name: string;
|
||
description: string;
|
||
creator_id: string;
|
||
category: string;
|
||
type: string;
|
||
app_url?: string;
|
||
icon?: string;
|
||
icon_color?: string;
|
||
}): Promise<{ success: boolean; app?: any; error?: string }> {
|
||
try {
|
||
const appId = crypto.randomUUID();
|
||
const sql = `
|
||
INSERT INTO apps (id, name, description, creator_id, category, type, app_url, icon, icon_color, likes_count, views_count, rating, is_active, created_at, updated_at)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0, 0.00, TRUE, NOW(), NOW())
|
||
`;
|
||
|
||
await this.query(sql, [
|
||
appId,
|
||
appData.name,
|
||
appData.description,
|
||
appData.creator_id,
|
||
appData.category,
|
||
appData.type,
|
||
appData.app_url || null,
|
||
appData.icon || 'Bot',
|
||
appData.icon_color || 'from-blue-500 to-purple-500'
|
||
]);
|
||
|
||
// 獲取創建的應用
|
||
const createdApp = await this.getAppById(appId);
|
||
|
||
return { success: true, app: createdApp };
|
||
} catch (error) {
|
||
console.error('創建應用錯誤:', error);
|
||
return { success: false, error: '創建應用時發生錯誤' };
|
||
}
|
||
}
|
||
|
||
// 根據 ID 獲取應用(僅已發布)
|
||
async getAppById(appId: string): Promise<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 = TRUE
|
||
`;
|
||
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 = TRUE
|
||
`;
|
||
return await this.queryOne(sql, [name]);
|
||
}
|
||
|
||
// 獲取所有應用
|
||
async getAllApps(filters: {
|
||
search?: string;
|
||
category?: string;
|
||
type?: string;
|
||
status?: string;
|
||
page?: number;
|
||
limit?: number;
|
||
} = {}): Promise<{ apps: any[]; total: number }> {
|
||
try {
|
||
const { search = '', category = 'all', type = 'all', status = 'all', page = 1, limit = 10 } = filters;
|
||
|
||
// 構建查詢條件
|
||
let whereConditions: string[] = [];
|
||
let params: any[] = [];
|
||
|
||
// 根據狀態篩選
|
||
if (status && status !== 'all') {
|
||
if (status === 'active') {
|
||
whereConditions.push('a.is_active = TRUE');
|
||
} else if (status === 'inactive') {
|
||
whereConditions.push('a.is_active = FALSE');
|
||
}
|
||
// 如果 status 是 'all' 或其他值,則不添加狀態篩選
|
||
}
|
||
|
||
if (search) {
|
||
whereConditions.push('(a.name LIKE ? OR a.description LIKE ? OR u.name LIKE ?)');
|
||
params.push(`%${search}%`, `%${search}%`, `%${search}%`);
|
||
}
|
||
|
||
if (category && category !== 'all') {
|
||
whereConditions.push('a.category = ?');
|
||
params.push(category);
|
||
}
|
||
|
||
if (type && type !== 'all') {
|
||
whereConditions.push('a.type = ?');
|
||
params.push(type);
|
||
}
|
||
|
||
const whereClause = whereConditions.length > 0 ? `WHERE ${whereConditions.join(' AND ')}` : '';
|
||
|
||
// 獲取總數
|
||
const countSql = `
|
||
SELECT COUNT(DISTINCT a.id) as total
|
||
FROM apps a
|
||
LEFT JOIN users u ON a.creator_id = u.id
|
||
LEFT JOIN user_ratings ur ON a.id = ur.app_id
|
||
${whereClause}
|
||
`;
|
||
const countResult = await this.queryOne(countSql, params);
|
||
const total = countResult?.total || 0;
|
||
|
||
// 獲取應用列表
|
||
const offset = (page - 1) * limit;
|
||
const sql = `
|
||
SELECT
|
||
a.*,
|
||
u.name as creator_name,
|
||
u.department as creator_department,
|
||
u.email as creator_email,
|
||
COALESCE(AVG(ur.rating), 0) as rating,
|
||
COUNT(ur.id) as reviewCount
|
||
FROM apps a
|
||
LEFT JOIN users u ON a.creator_id = u.id
|
||
LEFT JOIN user_ratings ur ON a.id = ur.app_id
|
||
${whereClause}
|
||
GROUP BY a.id, u.name, u.department, u.email
|
||
ORDER BY a.created_at DESC
|
||
LIMIT ${limit} OFFSET ${offset}
|
||
`;
|
||
|
||
const apps = await this.query(sql, params);
|
||
|
||
return { apps, total };
|
||
} catch (error) {
|
||
console.error('獲取應用列表錯誤:', error);
|
||
return { apps: [], total: 0 };
|
||
}
|
||
}
|
||
|
||
// 更新應用
|
||
async updateApp(appId: string, updates: {
|
||
name?: string;
|
||
description?: string;
|
||
category?: string;
|
||
type?: string;
|
||
app_url?: string;
|
||
icon?: string;
|
||
icon_color?: string;
|
||
}): Promise<{ success: boolean; app?: any; error?: string }> {
|
||
try {
|
||
const updateFields = [];
|
||
const params = [];
|
||
|
||
if (updates.name !== undefined) {
|
||
updateFields.push('name = ?');
|
||
params.push(updates.name);
|
||
}
|
||
if (updates.description !== undefined) {
|
||
updateFields.push('description = ?');
|
||
params.push(updates.description);
|
||
}
|
||
if (updates.category !== undefined) {
|
||
updateFields.push('category = ?');
|
||
params.push(updates.category);
|
||
}
|
||
if (updates.type !== undefined) {
|
||
updateFields.push('type = ?');
|
||
params.push(updates.type);
|
||
}
|
||
if (updates.app_url !== undefined) {
|
||
updateFields.push('app_url = ?');
|
||
params.push(updates.app_url);
|
||
}
|
||
if (updates.icon !== undefined) {
|
||
updateFields.push('icon = ?');
|
||
params.push(updates.icon);
|
||
}
|
||
if (updates.icon_color !== undefined) {
|
||
updateFields.push('icon_color = ?');
|
||
params.push(updates.icon_color);
|
||
}
|
||
|
||
if (updateFields.length === 0) {
|
||
return { success: false, error: '沒有要更新的欄位' };
|
||
}
|
||
|
||
updateFields.push('updated_at = NOW()');
|
||
params.push(appId);
|
||
|
||
const sql = `
|
||
UPDATE apps
|
||
SET ${updateFields.join(', ')}
|
||
WHERE id = ? AND is_active = TRUE
|
||
`;
|
||
|
||
await this.query(sql, params);
|
||
|
||
// 獲取更新後的應用
|
||
const updatedApp = await this.getAppById(appId);
|
||
|
||
return { success: true, app: updatedApp };
|
||
} catch (error) {
|
||
console.error('更新應用錯誤:', error);
|
||
return { success: false, error: '更新應用時發生錯誤' };
|
||
}
|
||
}
|
||
|
||
// 刪除應用(硬刪除)
|
||
async deleteApp(appId: string): Promise<{ success: boolean; error?: string }> {
|
||
try {
|
||
// 先檢查應用是否存在(任何狀態)
|
||
const app = await this.getAppByIdAnyStatus(appId);
|
||
if (!app) {
|
||
return { success: false, error: '應用不存在' };
|
||
}
|
||
|
||
// 硬刪除應用(從資料庫中完全移除)
|
||
const sql = 'DELETE FROM apps WHERE id = ?';
|
||
await this.query(sql, [appId]);
|
||
return { success: true };
|
||
} catch (error) {
|
||
console.error('刪除應用錯誤:', error);
|
||
return { success: false, error: '刪除應用時發生錯誤' };
|
||
}
|
||
}
|
||
|
||
// 切換應用狀態
|
||
async toggleAppStatus(appId: string): Promise<{ success: boolean; app?: any; error?: string }> {
|
||
try {
|
||
// 先獲取當前狀態(任何狀態)
|
||
const currentApp = await this.getAppByIdAnyStatus(appId);
|
||
if (!currentApp) {
|
||
return { success: false, error: '應用不存在' };
|
||
}
|
||
|
||
const newStatus = currentApp.is_active ? false : true;
|
||
const sql = 'UPDATE apps SET is_active = ?, updated_at = NOW() WHERE id = ?';
|
||
await this.query(sql, [newStatus, appId]);
|
||
|
||
// 獲取更新後的應用(任何狀態)
|
||
const updatedApp = await this.getAppByIdAnyStatus(appId);
|
||
|
||
return { success: true, app: updatedApp };
|
||
} catch (error) {
|
||
console.error('切換應用狀態錯誤:', error);
|
||
return { success: false, error: '切換應用狀態時發生錯誤' };
|
||
}
|
||
}
|
||
|
||
// 獲取應用統計
|
||
async getAppStats(): Promise<{
|
||
totalApps: number;
|
||
activeApps: number;
|
||
inactiveApps: number;
|
||
pendingApps: number;
|
||
totalViews: number;
|
||
totalLikes: number;
|
||
newThisMonth: number;
|
||
}> {
|
||
try {
|
||
const sql = `
|
||
SELECT
|
||
COUNT(*) as total_apps,
|
||
COUNT(CASE WHEN is_active = TRUE THEN 1 END) as active_apps,
|
||
COUNT(CASE WHEN is_active = FALSE THEN 1 END) as inactive_apps,
|
||
COALESCE(SUM(views_count), 0) as total_views,
|
||
COALESCE(SUM(likes_count), 0) as total_likes,
|
||
COUNT(CASE WHEN created_at >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) THEN 1 END) as new_this_month
|
||
FROM apps
|
||
`;
|
||
|
||
const result = await this.queryOne(sql);
|
||
|
||
return {
|
||
totalApps: result?.total_apps || 0,
|
||
activeApps: result?.active_apps || 0,
|
||
inactiveApps: result?.inactive_apps || 0,
|
||
pendingApps: 0, // 目前沒有 pending 狀態
|
||
totalViews: result?.total_views || 0,
|
||
totalLikes: result?.total_likes || 0,
|
||
newThisMonth: result?.new_this_month || 0
|
||
};
|
||
} catch (error) {
|
||
console.error('獲取應用統計錯誤:', error);
|
||
return {
|
||
totalApps: 0,
|
||
activeApps: 0,
|
||
inactiveApps: 0,
|
||
pendingApps: 0,
|
||
totalViews: 0,
|
||
totalLikes: 0,
|
||
newThisMonth: 0
|
||
};
|
||
}
|
||
}
|
||
|
||
// 通用查詢方法
|
||
async query<T = any>(sql: string, params: any[] = []): Promise<T[]> {
|
||
return await db.query<T>(sql, params);
|
||
}
|
||
|
||
// 通用單一查詢方法
|
||
async queryOne<T = any>(sql: string, params: any[] = []): Promise<T | null> {
|
||
return await db.queryOne<T>(sql, params);
|
||
}
|
||
|
||
// 獲取應用評分統計
|
||
async 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 = TRUE
|
||
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 = TRUE
|
||
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 = TRUE
|
||
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;
|
||
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 created_at >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) THEN 1 END) as new_apps_this_month,
|
||
COALESCE(SUM(views_count), 0) as total_views,
|
||
COALESCE(SUM(likes_count), 0) as total_likes
|
||
FROM apps
|
||
WHERE is_active = TRUE
|
||
`;
|
||
const appStats = await this.queryOne(appStatsSql);
|
||
|
||
// 評論統計
|
||
const reviewStatsSql = `
|
||
SELECT COUNT(*) as total_reviews
|
||
FROM user_ratings
|
||
`;
|
||
const reviewStats = await this.queryOne(reviewStatsSql);
|
||
|
||
// 競賽統計
|
||
const competitionStatsSql = `
|
||
SELECT
|
||
COUNT(*) as total_competitions,
|
||
COUNT(CASE WHEN status = 'active' OR status = 'ongoing' THEN 1 END) as active_competitions
|
||
FROM competitions
|
||
`;
|
||
const competitionStats = await this.queryOne(competitionStatsSql);
|
||
|
||
// 計算增長率(與上個月比較)
|
||
const lastMonthUsersSql = `
|
||
SELECT COUNT(*) as last_month_users
|
||
FROM users
|
||
WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL 60 DAY)
|
||
AND created_at < DATE_SUB(CURDATE(), INTERVAL 30 DAY)
|
||
`;
|
||
const lastMonthUsers = await this.queryOne(lastMonthUsersSql);
|
||
|
||
const currentMonthUsers = userStats.newThisMonth;
|
||
const growthRate = lastMonthUsers?.last_month_users > 0
|
||
? ((currentMonthUsers - lastMonthUsers.last_month_users) / lastMonthUsers.last_month_users) * 100
|
||
: 0;
|
||
|
||
return {
|
||
totalUsers: userStats.totalUsers,
|
||
activeUsers: userStats.activeUsers,
|
||
totalApps: appStats?.total_apps || 0,
|
||
totalCompetitions: competitionStats?.total_competitions || 0,
|
||
totalReviews: reviewStats?.total_reviews || 0,
|
||
totalViews: appStats?.total_views || 0,
|
||
totalLikes: appStats?.total_likes || 0,
|
||
newAppsThisMonth: appStats?.new_apps_this_month || 0,
|
||
activeCompetitions: competitionStats?.active_competitions || 0,
|
||
growthRate: Math.round(growthRate * 100) / 100
|
||
};
|
||
} catch (error) {
|
||
console.error('獲取儀表板統計數據錯誤:', error);
|
||
return {
|
||
totalUsers: 0,
|
||
activeUsers: 0,
|
||
totalApps: 0,
|
||
totalCompetitions: 0,
|
||
totalReviews: 0,
|
||
totalViews: 0,
|
||
totalLikes: 0,
|
||
newAppsThisMonth: 0,
|
||
activeCompetitions: 0,
|
||
growthRate: 0
|
||
};
|
||
}
|
||
}
|
||
|
||
// 添加收藏
|
||
async 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 = TRUE
|
||
`;
|
||
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 = TRUE
|
||
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 };
|
||
}
|
||
}
|
||
|
||
// 獲取應用的收藏數量
|
||
async getAppFavoritesCount(appId: string): Promise<number> {
|
||
try {
|
||
const sql = `
|
||
SELECT COUNT(*) as count
|
||
FROM user_favorites
|
||
WHERE app_id = ?
|
||
`;
|
||
|
||
const result = await this.queryOne(sql, [appId]);
|
||
return result.count || 0;
|
||
} catch (error) {
|
||
console.error('獲取應用收藏數量錯誤:', error);
|
||
return 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.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 = TRUE';
|
||
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.category || '未分類';
|
||
}
|
||
|
||
if (!categoryCount[category]) {
|
||
categoryCount[category] = { count: 0, uniqueApps: new Set() };
|
||
}
|
||
categoryCount[category].count++;
|
||
categoryCount[category].uniqueApps.add(activity.resource_id);
|
||
} catch (error) {
|
||
// 出錯時使用默認類別
|
||
const category = '未分類';
|
||
if (!categoryCount[category]) {
|
||
categoryCount[category] = { count: 0, uniqueApps: new Set() };
|
||
}
|
||
categoryCount[category].count++;
|
||
categoryCount[category].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 {
|
||
// 提交應用評分
|
||
static async submitAppScore(scoreData: Omit<AppJudgeScore, 'id' | 'submitted_at'>): Promise<AppJudgeScore> {
|
||
const sql = `
|
||
INSERT INTO app_judge_scores (id, judge_id, app_id, innovation_score, technical_score, usability_score, presentation_score, impact_score, total_score, comments)
|
||
VALUES (UUID(), ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||
ON DUPLICATE KEY UPDATE
|
||
innovation_score = VALUES(innovation_score),
|
||
technical_score = VALUES(technical_score),
|
||
usability_score = VALUES(usability_score),
|
||
presentation_score = VALUES(presentation_score),
|
||
impact_score = VALUES(impact_score),
|
||
total_score = VALUES(total_score),
|
||
comments = VALUES(comments),
|
||
submitted_at = CURRENT_TIMESTAMP
|
||
`;
|
||
const params = [
|
||
scoreData.judge_id,
|
||
scoreData.app_id,
|
||
scoreData.innovation_score,
|
||
scoreData.technical_score,
|
||
scoreData.usability_score,
|
||
scoreData.presentation_score,
|
||
scoreData.impact_score,
|
||
scoreData.total_score,
|
||
scoreData.comments || null
|
||
];
|
||
|
||
await db.insert(sql, params);
|
||
return await this.getAppScore(scoreData.judge_id, scoreData.app_id) as AppJudgeScore;
|
||
}
|
||
|
||
// 獲取應用評分
|
||
static async getAppScore(judgeId: string, appId: string): Promise<AppJudgeScore | null> {
|
||
const sql = 'SELECT * FROM app_judge_scores WHERE judge_id = ? AND app_id = ?';
|
||
return await db.queryOne<AppJudgeScore>(sql, [judgeId, appId]);
|
||
}
|
||
|
||
// 獲取應用的所有評分
|
||
static async getAppScores(appId: string): Promise<AppJudgeScore[]> {
|
||
const sql = 'SELECT * FROM app_judge_scores WHERE app_id = ? ORDER BY submitted_at DESC';
|
||
return await db.query<AppJudgeScore>(sql, [appId]);
|
||
}
|
||
|
||
// 提交提案評分
|
||
static async submitProposalScore(scoreData: Omit<ProposalJudgeScore, 'id' | 'submitted_at'>): Promise<ProposalJudgeScore> {
|
||
const sql = `
|
||
INSERT INTO proposal_judge_scores (id, judge_id, proposal_id, problem_identification_score, solution_feasibility_score, innovation_score, impact_score, presentation_score, total_score, comments)
|
||
VALUES (UUID(), ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||
ON DUPLICATE KEY UPDATE
|
||
problem_identification_score = VALUES(problem_identification_score),
|
||
solution_feasibility_score = VALUES(solution_feasibility_score),
|
||
innovation_score = VALUES(innovation_score),
|
||
impact_score = VALUES(impact_score),
|
||
presentation_score = VALUES(presentation_score),
|
||
total_score = VALUES(total_score),
|
||
comments = VALUES(comments),
|
||
submitted_at = CURRENT_TIMESTAMP
|
||
`;
|
||
const params = [
|
||
scoreData.judge_id,
|
||
scoreData.proposal_id,
|
||
scoreData.problem_identification_score,
|
||
scoreData.solution_feasibility_score,
|
||
scoreData.innovation_score,
|
||
scoreData.impact_score,
|
||
scoreData.presentation_score,
|
||
scoreData.total_score,
|
||
scoreData.comments || null
|
||
];
|
||
|
||
await db.insert(sql, params);
|
||
return await this.getProposalScore(scoreData.judge_id, scoreData.proposal_id) as ProposalJudgeScore;
|
||
}
|
||
|
||
// 獲取提案評分
|
||
static async getProposalScore(judgeId: string, proposalId: string): Promise<ProposalJudgeScore | null> {
|
||
const sql = 'SELECT * FROM proposal_judge_scores WHERE judge_id = ? AND proposal_id = ?';
|
||
return await db.queryOne<ProposalJudgeScore>(sql, [judgeId, proposalId]);
|
||
}
|
||
|
||
// 獲取提案的所有評分
|
||
static async getProposalScores(proposalId: string): Promise<ProposalJudgeScore[]> {
|
||
const sql = 'SELECT * FROM proposal_judge_scores WHERE proposal_id = ? ORDER BY submitted_at DESC';
|
||
return await db.query<ProposalJudgeScore>(sql, [proposalId]);
|
||
}
|
||
}
|
||
|
||
// =====================================================
|
||
// 獎項服務
|
||
// =====================================================
|
||
export class AwardService {
|
||
// 創建獎項
|
||
static async createAward(awardData: Omit<Award, 'id' | 'created_at'>): Promise<Award> {
|
||
const sql = `
|
||
INSERT INTO awards (id, competition_id, app_id, team_id, proposal_id, app_name, team_name, proposal_title, creator, award_type, award_name, score, year, month, icon, custom_award_type_id, competition_type, rank, category)
|
||
VALUES (UUID(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||
`;
|
||
const params = [
|
||
awardData.competition_id,
|
||
awardData.app_id || null,
|
||
awardData.team_id || null,
|
||
awardData.proposal_id || null,
|
||
awardData.app_name || null,
|
||
awardData.team_name || null,
|
||
awardData.proposal_title || null,
|
||
awardData.creator,
|
||
awardData.award_type,
|
||
awardData.award_name,
|
||
awardData.score,
|
||
awardData.year,
|
||
awardData.month,
|
||
awardData.icon,
|
||
awardData.custom_award_type_id || null,
|
||
awardData.competition_type,
|
||
awardData.rank,
|
||
awardData.category
|
||
];
|
||
|
||
await db.insert(sql, params);
|
||
return await this.getAwardByCompetitionAndCreator(awardData.competition_id, awardData.creator) as Award;
|
||
}
|
||
|
||
// 根據競賽和創作者獲取獎項
|
||
static async getAwardByCompetitionAndCreator(competitionId: string, creator: string): Promise<Award | null> {
|
||
const sql = 'SELECT * FROM awards WHERE competition_id = ? AND creator = ? ORDER BY created_at DESC LIMIT 1';
|
||
return await db.queryOne<Award>(sql, [competitionId, creator]);
|
||
}
|
||
|
||
// 根據年份獲取獎項
|
||
static async getAwardsByYear(year: number): Promise<Award[]> {
|
||
const sql = 'SELECT * FROM awards WHERE year = ? ORDER BY month DESC, rank ASC';
|
||
return await db.query<Award>(sql, [year]);
|
||
}
|
||
|
||
// 根據競賽獲取獎項
|
||
static async getAwardsByCompetition(competitionId: string): Promise<Award[]> {
|
||
const sql = 'SELECT * FROM awards WHERE competition_id = ? ORDER BY rank ASC';
|
||
return await db.query<Award>(sql, [competitionId]);
|
||
}
|
||
}
|
||
|
||
// =====================================================
|
||
// 系統設定服務
|
||
// =====================================================
|
||
export class SystemSettingService {
|
||
// 獲取設定值
|
||
static async getSetting(key: string): Promise<string | null> {
|
||
const sql = 'SELECT value FROM system_settings WHERE `key` = ?';
|
||
const result = await db.queryOne<{ value: string }>(sql, [key]);
|
||
return result?.value || null;
|
||
}
|
||
|
||
// 設定值
|
||
static async setSetting(key: string, value: string, description?: string, category = 'general', isPublic = false): Promise<boolean> {
|
||
const sql = `
|
||
INSERT INTO system_settings (id, \`key\`, value, description, category, is_public)
|
||
VALUES (UUID(), ?, ?, ?, ?, ?)
|
||
ON DUPLICATE KEY UPDATE
|
||
value = VALUES(value),
|
||
description = VALUES(description),
|
||
category = VALUES(category),
|
||
is_public = VALUES(is_public),
|
||
updated_at = CURRENT_TIMESTAMP
|
||
`;
|
||
const params = [key, value, description || null, category, isPublic];
|
||
|
||
const result = await db.insert(sql, params);
|
||
return result.affectedRows > 0;
|
||
}
|
||
|
||
// 獲取所有公開設定
|
||
static async getPublicSettings(): Promise<SystemSetting[]> {
|
||
const sql = 'SELECT * FROM system_settings WHERE is_public = TRUE ORDER BY category, `key`';
|
||
return await db.query<SystemSetting>(sql);
|
||
}
|
||
}
|