實作管理者用戶管理、邀請註冊功能
This commit is contained in:
@@ -3,6 +3,8 @@
|
||||
// =====================================================
|
||||
|
||||
import { db } from '../database';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import crypto from 'crypto';
|
||||
import type {
|
||||
User,
|
||||
Judge,
|
||||
@@ -37,7 +39,7 @@ export class UserService {
|
||||
// 創建用戶
|
||||
async create(userData: Omit<User, 'id' | 'created_at' | 'updated_at'>): Promise<User> {
|
||||
const sql = `
|
||||
INSERT INTO users (id, name, email, password_hash, avatar, department, role, join_date, total_likes, total_views, is_active, last_login)
|
||||
INSERT INTO users (id, name, email, password_hash, avatar, department, role, join_date, total_likes, total_views, status, last_login)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`;
|
||||
const params = [
|
||||
@@ -51,7 +53,7 @@ export class UserService {
|
||||
userData.join_date,
|
||||
userData.total_likes,
|
||||
userData.total_views,
|
||||
userData.is_active,
|
||||
userData.status,
|
||||
userData.last_login || null
|
||||
];
|
||||
|
||||
@@ -61,13 +63,13 @@ export class UserService {
|
||||
|
||||
// 根據郵箱獲取用戶
|
||||
async findByEmail(email: string): Promise<User | null> {
|
||||
const sql = 'SELECT * FROM users WHERE email = ? AND is_active = TRUE';
|
||||
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 is_active = TRUE';
|
||||
const sql = 'SELECT * FROM users WHERE id = ? AND status = "active"';
|
||||
return await db.queryOne<User>(sql, [id]);
|
||||
}
|
||||
|
||||
@@ -111,7 +113,7 @@ export class UserService {
|
||||
const { search, department, role, status, page = 1, limit = 10 } = filters;
|
||||
|
||||
// 構建查詢條件
|
||||
let whereConditions = ['is_active = TRUE'];
|
||||
let whereConditions: string[] = [];
|
||||
let params: any[] = [];
|
||||
|
||||
if (search) {
|
||||
@@ -131,9 +133,14 @@ export class UserService {
|
||||
|
||||
if (status && status !== 'all') {
|
||||
if (status === 'active') {
|
||||
whereConditions.push('last_login IS NOT NULL AND last_login >= DATE_SUB(NOW(), INTERVAL 30 DAY)');
|
||||
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('last_login IS NULL OR last_login < DATE_SUB(NOW(), INTERVAL 30 DAY)');
|
||||
whereConditions.push('status = ?');
|
||||
params.push('inactive');
|
||||
} else if (status === 'invited') {
|
||||
whereConditions.push('status = ?');
|
||||
params.push('invited');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,7 +156,7 @@ export class UserService {
|
||||
const usersSql = `
|
||||
SELECT
|
||||
id, name, email, avatar, department, role, join_date,
|
||||
total_likes, total_views, is_active, last_login, created_at, updated_at
|
||||
total_likes, total_views, status, last_login, created_at, updated_at
|
||||
FROM users
|
||||
${whereClause}
|
||||
ORDER BY created_at DESC
|
||||
@@ -166,19 +173,20 @@ export class UserService {
|
||||
activeUsers: number;
|
||||
adminCount: number;
|
||||
developerCount: number;
|
||||
invitedUsers: number;
|
||||
inactiveUsers: number;
|
||||
newThisMonth: number;
|
||||
}> {
|
||||
const sql = `
|
||||
SELECT
|
||||
COUNT(*) as total_users,
|
||||
COUNT(CASE WHEN last_login IS NOT NULL AND last_login >= DATE_SUB(NOW(), INTERVAL 30 DAY) THEN 1 END) as active_users,
|
||||
COUNT(CASE WHEN 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 last_login IS NULL OR last_login < DATE_SUB(NOW(), INTERVAL 30 DAY) THEN 1 END) as inactive_users,
|
||||
COUNT(CASE WHEN created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY) THEN 1 END) as new_this_month
|
||||
FROM users
|
||||
WHERE is_active = TRUE
|
||||
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] || {};
|
||||
@@ -188,16 +196,544 @@ export class UserService {
|
||||
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(view_count), 0) as total_views,
|
||||
COALESCE(SUM(like_count), 0) as total_likes
|
||||
FROM apps
|
||||
WHERE is_active = TRUE
|
||||
`;
|
||||
const appStats = await this.queryOne(appStatsSql);
|
||||
|
||||
// 評論統計
|
||||
const reviewStatsSql = `
|
||||
SELECT COUNT(*) as total_reviews
|
||||
FROM user_ratings
|
||||
`;
|
||||
const reviewStats = await this.queryOne(reviewStatsSql);
|
||||
|
||||
// 競賽統計
|
||||
const competitionStatsSql = `
|
||||
SELECT
|
||||
COUNT(*) as total_competitions,
|
||||
COUNT(CASE WHEN status = 'active' THEN 1 END) as active_competitions
|
||||
FROM competitions
|
||||
`;
|
||||
const competitionStats = await this.queryOne(competitionStatsSql);
|
||||
|
||||
// 計算增長率(與上個月比較)
|
||||
const lastMonthUsersSql = `
|
||||
SELECT COUNT(*) as last_month_users
|
||||
FROM users
|
||||
WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL 60 DAY)
|
||||
AND created_at < DATE_SUB(CURDATE(), INTERVAL 30 DAY)
|
||||
`;
|
||||
const lastMonthUsers = await this.queryOne(lastMonthUsersSql);
|
||||
|
||||
const currentMonthUsers = userStats.newThisMonth;
|
||||
const growthRate = lastMonthUsers?.last_month_users > 0
|
||||
? Math.round(((currentMonthUsers - lastMonthUsers.last_month_users) / lastMonthUsers.last_month_users) * 100)
|
||||
: 0;
|
||||
|
||||
return {
|
||||
totalUsers: userStats.totalUsers,
|
||||
activeUsers: userStats.activeUsers,
|
||||
totalApps: appStats?.total_apps || 0,
|
||||
totalCompetitions: competitionStats?.total_competitions || 0,
|
||||
totalReviews: reviewStats?.total_reviews || 0,
|
||||
totalViews: appStats?.total_views || 0,
|
||||
totalLikes: appStats?.total_likes || 0,
|
||||
newAppsThisMonth: appStats?.new_apps_this_month || 0,
|
||||
activeCompetitions: competitionStats?.active_competitions || 0,
|
||||
growthRate: growthRate
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('獲取儀表板統計數據錯誤:', error);
|
||||
return {
|
||||
totalUsers: 0,
|
||||
activeUsers: 0,
|
||||
totalApps: 0,
|
||||
totalCompetitions: 0,
|
||||
totalReviews: 0,
|
||||
totalViews: 0,
|
||||
totalLikes: 0,
|
||||
newAppsThisMonth: 0,
|
||||
activeCompetitions: 0,
|
||||
growthRate: 0
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 獲取最新活動
|
||||
async getRecentActivities(limit: number = 10): Promise<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 ?
|
||||
`;
|
||||
|
||||
const activities = await this.query(sql, [limit]);
|
||||
|
||||
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.view_count as views,
|
||||
a.like_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.view_count, a.like_count, a.category, a.created_at
|
||||
ORDER BY (a.view_count + a.like_count * 2) DESC
|
||||
LIMIT ?
|
||||
`;
|
||||
|
||||
const apps = await this.query(sql, [limit]);
|
||||
|
||||
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);
|
||||
|
Reference in New Issue
Block a user