實作管理者用戶管理、邀請註冊功能

This commit is contained in:
2025-09-09 15:15:26 +08:00
parent 32b19e9a0f
commit 46bd9db2e3
11 changed files with 1297 additions and 214 deletions

View File

@@ -14,7 +14,7 @@ export interface User {
join_date: string;
total_likes: number;
total_views: number;
is_active: boolean;
status: 'active' | 'inactive' | 'invited';
last_login?: string;
phone?: string;
location?: string;
@@ -34,7 +34,7 @@ export interface UserProfile {
join_date: string;
total_likes: number;
total_views: number;
is_active: boolean;
status: 'active' | 'inactive' | 'invited';
last_login?: string;
phone?: string;
location?: string;

View File

@@ -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);