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

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

@@ -0,0 +1,33 @@
import { NextRequest, NextResponse } from 'next/server'
import { UserService } from '@/lib/services/database-service'
const userService = new UserService()
export async function GET(request: NextRequest) {
try {
// 獲取儀表板統計數據
const stats = await userService.getDashboardStats()
// 獲取最新活動
const recentActivities = await userService.getRecentActivities(10)
// 獲取熱門應用
const topApps = await userService.getTopApps(5)
return NextResponse.json({
success: true,
data: {
stats,
recentActivities,
topApps
}
})
} catch (error) {
console.error('獲取儀表板數據錯誤:', error)
return NextResponse.json(
{ success: false, error: '獲取儀表板數據時發生錯誤' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,46 @@
import { NextRequest, NextResponse } from 'next/server'
import { UserService } from '@/lib/services/database-service'
const userService = new UserService()
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id: userId } = await params
const { searchParams } = new URL(request.url)
const page = parseInt(searchParams.get('page') || '1')
const limit = parseInt(searchParams.get('limit') || '10')
const search = searchParams.get('search') || ''
const startDate = searchParams.get('startDate') || ''
const endDate = searchParams.get('endDate') || ''
const activityType = searchParams.get('activityType') || 'all'
if (!userId) {
return NextResponse.json(
{ success: false, error: '用戶 ID 是必需的' },
{ status: 400 }
)
}
const result = await userService.getUserActivities(userId, page, limit, {
search,
startDate,
endDate,
activityType
})
return NextResponse.json({
success: true,
data: result
})
} catch (error) {
console.error('獲取用戶活動記錄錯誤:', error)
return NextResponse.json(
{ success: false, error: '獲取用戶活動記錄時發生錯誤' },
{ status: 500 }
)
}
}

View File

@@ -3,71 +3,71 @@ import { UserService } from '@/lib/services/database-service'
const userService = new UserService() const userService = new UserService()
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const user = await userService.findById(params.id)
if (!user) {
return NextResponse.json(
{ error: '用戶不存在' },
{ status: 404 }
)
}
// 獲取用戶統計
const stats = await userService.getUserStatistics(params.id)
return NextResponse.json({
success: true,
data: {
user,
stats
}
})
} catch (error) {
console.error('獲取用戶詳情錯誤:', error)
return NextResponse.json(
{ error: '獲取用戶詳情時發生錯誤' },
{ status: 500 }
)
}
}
export async function PUT( export async function PUT(
request: NextRequest, request: NextRequest,
{ params }: { params: { id: string } } { params }: { params: Promise<{ id: string }> }
) { ) {
try { try {
const updates = await request.json() const { id: userId } = await params
const body = await request.json()
const { name, department, role, status } = body
// 移除不允許更新的欄位 if (!userId) {
delete updates.id
delete updates.created_at
delete updates.password_hash
const updatedUser = await userService.update(params.id, updates)
if (!updatedUser) {
return NextResponse.json( return NextResponse.json(
{ error: '用戶不存在或更新失敗' }, { success: false, error: '用戶 ID 是必需的' },
{ status: 404 } { status: 400 }
) )
} }
if (!name || !department || !role || !status) {
return NextResponse.json(
{ success: false, error: '請填寫所有必填欄位' },
{ status: 400 }
)
}
// 驗證狀態值
const validStatuses = ['active', 'inactive', 'invited']
if (!validStatuses.includes(status)) {
return NextResponse.json(
{ success: false, error: '無效的狀態值' },
{ status: 400 }
)
}
// 驗證角色值
const validRoles = ['user', 'developer', 'admin']
if (!validRoles.includes(role)) {
return NextResponse.json(
{ success: false, error: '無效的角色值' },
{ status: 400 }
)
}
const result = await userService.updateUser(userId, {
name,
department,
role,
status
})
if (result.success) {
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
message: '用戶資料更新', message: '用戶資料更新成功',
data: updatedUser data: result.user
}) })
} else {
return NextResponse.json(
{ success: false, error: result.error },
{ status: 400 }
)
}
} catch (error) { } catch (error) {
console.error('更新用戶錯誤:', error) console.error('更新用戶錯誤:', error)
return NextResponse.json( return NextResponse.json(
{ error: '更新用戶時發生錯誤' }, { success: false, error: '更新用戶時發生錯誤' },
{ status: 500 } { status: 500 }
) )
} }
@@ -75,28 +75,36 @@ export async function PUT(
export async function DELETE( export async function DELETE(
request: NextRequest, request: NextRequest,
{ params }: { params: { id: string } } { params }: { params: Promise<{ id: string }> }
) { ) {
try { try {
// 軟刪除:將 is_active 設為 false const { id: userId } = await params
const result = await userService.update(params.id, { is_active: false })
if (!result) { if (!userId) {
return NextResponse.json( return NextResponse.json(
{ error: '用戶不存在或刪除失敗' }, { success: false, error: '用戶 ID 是必需的' },
{ status: 404 } { status: 400 }
) )
} }
const result = await userService.deleteUser(userId)
if (result.success) {
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
message: '用戶刪除' message: '用戶刪除成功'
}) })
} else {
return NextResponse.json(
{ success: false, error: result.error },
{ status: 400 }
)
}
} catch (error) { } catch (error) {
console.error('刪除用戶錯誤:', error) console.error('刪除用戶錯誤:', error)
return NextResponse.json( return NextResponse.json(
{ error: '刪除用戶時發生錯誤' }, { success: false, error: '刪除用戶時發生錯誤' },
{ status: 500 } { status: 500 }
) )
} }

View File

@@ -0,0 +1,34 @@
import { NextRequest, NextResponse } from 'next/server'
import { UserService } from '@/lib/services/database-service'
const userService = new UserService()
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id: userId } = await params
if (!userId) {
return NextResponse.json(
{ success: false, error: '用戶 ID 是必需的' },
{ status: 400 }
)
}
const stats = await userService.getUserDetailedStats(userId)
return NextResponse.json({
success: true,
data: stats
})
} catch (error) {
console.error('獲取用戶統計數據錯誤:', error)
return NextResponse.json(
{ success: false, error: '獲取用戶統計數據時發生錯誤' },
{ status: 500 }
)
}
}

View File

@@ -3,6 +3,73 @@ import { UserService } from '@/lib/services/database-service'
const userService = new UserService() const userService = new UserService()
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { email, role } = body
if (!email || !role) {
return NextResponse.json(
{ success: false, error: '請提供電子郵件和角色' },
{ status: 400 }
)
}
// 驗證電子郵件格式
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(email)) {
return NextResponse.json(
{ success: false, error: '請輸入有效的電子郵件格式' },
{ status: 400 }
)
}
// 驗證角色
const validRoles = ['user', 'developer', 'admin']
if (!validRoles.includes(role)) {
return NextResponse.json(
{ success: false, error: '無效的角色值' },
{ status: 400 }
)
}
// 檢查電子郵件是否已存在
const existingUser = await userService.findByEmail(email)
if (existingUser) {
return NextResponse.json(
{ success: false, error: '此電子郵件已被使用' },
{ status: 400 }
)
}
// 創建邀請用戶
const result = await userService.createInvitedUser(email, role)
if (result.success) {
return NextResponse.json({
success: true,
message: '邀請用戶創建成功',
data: {
user: result.user,
invitationLink: result.invitationLink
}
})
} else {
return NextResponse.json(
{ success: false, error: result.error },
{ status: 400 }
)
}
} catch (error) {
console.error('創建邀請用戶錯誤:', error)
return NextResponse.json(
{ success: false, error: '創建邀請用戶時發生錯誤' },
{ status: 500 }
)
}
}
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
try { try {
const { searchParams } = new URL(request.url) const { searchParams } = new URL(request.url)
@@ -14,7 +81,7 @@ export async function GET(request: NextRequest) {
const status = searchParams.get('status') || '' const status = searchParams.get('status') || ''
// 構建查詢條件 // 構建查詢條件
let whereConditions = ['is_active = TRUE'] let whereConditions = ['status IN ("active", "inactive", "invited")']
let params: any[] = [] let params: any[] = []
if (search) { if (search) {
@@ -34,9 +101,11 @@ export async function GET(request: NextRequest) {
if (status && status !== 'all') { if (status && status !== 'all') {
if (status === 'active') { if (status === 'active') {
whereConditions.push('last_login IS NOT NULL AND last_login >= DATE_SUB(NOW(), INTERVAL 30 DAY)') whereConditions.push('status = "active"')
} else if (status === 'inactive') { } else if (status === 'inactive') {
whereConditions.push('last_login IS NULL OR last_login < DATE_SUB(NOW(), INTERVAL 30 DAY)') whereConditions.push('status = "inactive"')
} else if (status === 'invited') {
whereConditions.push('status = "invited"')
} }
} }
@@ -54,10 +123,21 @@ export async function GET(request: NextRequest) {
const stats = await userService.getUserStats() const stats = await userService.getUserStats()
// 映射用戶數據欄位,將資料庫欄位轉換為前端期望的欄位
const mappedUsers = users.map(user => ({
...user,
joinDate: user.join_date ? new Date(user.join_date).toLocaleDateString('zh-TW') : '-',
lastLogin: user.last_login ? new Date(user.last_login).toLocaleString('zh-TW') : '-',
status: user.status, // 直接使用資料庫的 status 欄位
totalApps: 0, // 暫時設為 0後續可以從統計數據中獲取
totalReviews: 0, // 暫時設為 0後續可以從統計數據中獲取
totalLikes: user.total_likes || 0
}))
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
data: { data: {
users, users: mappedUsers,
pagination: { pagination: {
page, page,
limit, limit,
@@ -65,12 +145,12 @@ export async function GET(request: NextRequest) {
totalPages: Math.ceil(total / limit) totalPages: Math.ceil(total / limit)
}, },
stats: { stats: {
totalUsers: stats?.total_users || 0, totalUsers: stats?.totalUsers || 0,
activeUsers: stats?.active_users || 0, activeUsers: stats?.activeUsers || 0,
adminCount: stats?.admin_count || 0, adminCount: stats?.adminCount || 0,
developerCount: stats?.developer_count || 0, developerCount: stats?.developerCount || 0,
inactiveUsers: stats?.inactive_users || 0, inactiveUsers: stats?.invitedUsers || 0, // 將 invitedUsers 映射為 inactiveUsers待註冊
newThisMonth: stats?.new_this_month || 0 newThisMonth: stats?.newThisMonth || 0
} }
} }
}) })
@@ -84,51 +164,3 @@ export async function GET(request: NextRequest) {
} }
} }
export async function POST(request: NextRequest) {
try {
const { email, role } = await request.json()
if (!email || !role) {
return NextResponse.json(
{ error: '請提供電子郵件和角色' },
{ status: 400 }
)
}
// 檢查郵箱是否已存在
const existingUser = await userService.findByEmail(email)
if (existingUser) {
return NextResponse.json(
{ error: '該電子郵件地址已被使用' },
{ status: 400 }
)
}
// 生成邀請 token
const { v4: uuidv4 } = require('uuid')
const invitationToken = uuidv4()
// 創建邀請記錄(這裡可以存儲到邀請表或臨時表)
// 暫時返回邀請連結
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
const invitationLink = `${baseUrl}/register?token=${invitationToken}&email=${encodeURIComponent(email)}&role=${encodeURIComponent(role)}`
return NextResponse.json({
success: true,
message: '用戶邀請已創建',
data: {
invitationLink,
token: invitationToken,
email,
role
}
})
} catch (error) {
console.error('創建用戶邀請錯誤:', error)
return NextResponse.json(
{ error: '創建用戶邀請時發生錯誤' },
{ status: 500 }
)
}
}

View File

@@ -23,7 +23,37 @@ export async function POST(request: NextRequest) {
) )
} }
// 檢查用戶是否已存在 // 加密密碼
const saltRounds = 12
const password_hash = await bcrypt.hash(password, saltRounds)
// 檢查是否為邀請用戶(狀態為 invited
const invitedUser = await userService.findInvitedUserByEmail(email)
if (invitedUser) {
// 更新邀請用戶為正式用戶
const updatedUser = await userService.completeInvitedUserRegistration(
invitedUser.id,
name,
department,
password_hash,
role
)
if (updatedUser.success) {
const { password_hash: _, ...userWithoutPassword } = updatedUser.user
return NextResponse.json({
success: true,
user: userWithoutPassword
})
} else {
return NextResponse.json(
{ error: updatedUser.error || '完成註冊失敗' },
{ status: 400 }
)
}
} else {
// 檢查用戶是否已存在(活躍用戶)
const existingUser = await userService.findByEmail(email) const existingUser = await userService.findByEmail(email)
if (existingUser) { if (existingUser) {
return NextResponse.json( return NextResponse.json(
@@ -32,10 +62,6 @@ export async function POST(request: NextRequest) {
) )
} }
// 加密密碼
const saltRounds = 12
const password_hash = await bcrypt.hash(password, saltRounds)
// 創建新用戶 // 創建新用戶
const newUser = { const newUser = {
id: uuidv4(), id: uuidv4(),
@@ -47,7 +73,7 @@ export async function POST(request: NextRequest) {
join_date: new Date().toISOString().split('T')[0], join_date: new Date().toISOString().split('T')[0],
total_likes: 0, total_likes: 0,
total_views: 0, total_views: 0,
is_active: true status: 'active' as 'active' | 'inactive' | 'invited'
} }
const createdUser = await userService.create(newUser) const createdUser = await userService.create(newUser)
@@ -58,6 +84,7 @@ export async function POST(request: NextRequest) {
success: true, success: true,
user: userWithoutPassword user: userWithoutPassword
}) })
}
} catch (error) { } catch (error) {
console.error('註冊錯誤:', error) console.error('註冊錯誤:', error)

View File

@@ -130,6 +130,7 @@ export default function RegisterPage() {
email: formData.email, email: formData.email,
password: formData.password, password: formData.password,
department: formData.department, department: formData.department,
role: isInvitedUser ? invitedRole : 'user', // 如果是邀請用戶,使用邀請的角色
}) })
if (success) { if (success) {

View File

@@ -33,9 +33,17 @@ import {
RefreshCw, RefreshCw,
Copy, Copy,
Link, Link,
Heart,
ThumbsUp,
Star,
Plus,
ExternalLink, ExternalLink,
Code, Code,
Users, Users,
ChevronLeft,
ChevronRight,
Filter,
X,
} from "lucide-react" } from "lucide-react"
export function UserManagement() { export function UserManagement() {
@@ -45,6 +53,25 @@ export function UserManagement() {
const [selectedRole, setSelectedRole] = useState("all") const [selectedRole, setSelectedRole] = useState("all")
const [selectedStatus, setSelectedStatus] = useState("all") const [selectedStatus, setSelectedStatus] = useState("all")
const [selectedUser, setSelectedUser] = useState<any>(null) const [selectedUser, setSelectedUser] = useState<any>(null)
const [userActivities, setUserActivities] = useState<any[]>([])
const [userStats, setUserStats] = useState<any>({
totalApps: 0,
totalReviews: 0,
totalLikes: 0,
loginDays: 0
})
const [activityPagination, setActivityPagination] = useState({
page: 1,
limit: 10,
total: 0,
totalPages: 0
})
const [activityFilters, setActivityFilters] = useState({
search: '',
startDate: '',
endDate: '',
activityType: 'all'
})
const [showUserDetail, setShowUserDetail] = useState(false) const [showUserDetail, setShowUserDetail] = useState(false)
const [showInviteUser, setShowInviteUser] = useState(false) const [showInviteUser, setShowInviteUser] = useState(false)
const [showEditUser, setShowEditUser] = useState(false) const [showEditUser, setShowEditUser] = useState(false)
@@ -71,6 +98,74 @@ export function UserManagement() {
setIsClient(true) setIsClient(true)
}, []) }, [])
// 載入用戶統計數據
const loadUserStats = async (userId: string) => {
try {
const response = await fetch(`/api/admin/users/${userId}/stats`)
const data = await response.json()
if (data.success) {
setUserStats(data.data)
} else {
console.error('載入用戶統計數據失敗:', data.error)
setUserStats({
totalApps: 0,
totalReviews: 0,
totalLikes: 0,
loginDays: 0
})
}
} catch (error) {
console.error('載入用戶統計數據錯誤:', error)
setUserStats({
totalApps: 0,
totalReviews: 0,
totalLikes: 0,
loginDays: 0
})
}
}
// 載入用戶活動記錄
const loadUserActivities = async (userId: string, page: number = 1, filters: any = {}) => {
try {
const params = new URLSearchParams({
page: page.toString(),
limit: activityPagination.limit.toString(),
search: filters.search || '',
startDate: filters.startDate || '',
endDate: filters.endDate || '',
activityType: filters.activityType || 'all'
})
const response = await fetch(`/api/admin/users/${userId}/activities?${params}`)
const data = await response.json()
if (data.success) {
setUserActivities(data.data.activities)
setActivityPagination(data.data.pagination)
} else {
console.error('載入活動記錄失敗:', data.error)
setUserActivities([])
setActivityPagination({
page: 1,
limit: 10,
total: 0,
totalPages: 0
})
}
} catch (error) {
console.error('載入活動記錄錯誤:', error)
setUserActivities([])
setActivityPagination({
page: 1,
limit: 10,
total: 0,
totalPages: 0
})
}
}
// 載入用戶數據 // 載入用戶數據
const loadUsers = async () => { const loadUsers = async () => {
if (!isClient) return if (!isClient) return
@@ -133,6 +228,51 @@ export function UserManagement() {
const handleViewUser = (user: any) => { const handleViewUser = (user: any) => {
setSelectedUser(user) setSelectedUser(user)
setShowUserDetail(true) setShowUserDetail(true)
// 重置活動記錄狀態
setActivityPagination({
page: 1,
limit: 10,
total: 0,
totalPages: 0
})
setActivityFilters({
search: '',
startDate: '',
endDate: '',
activityType: 'all'
})
// 重置統計數據狀態
setUserStats({
totalApps: 0,
totalReviews: 0,
totalLikes: 0,
loginDays: 0
})
// 載入用戶統計數據和活動記錄
loadUserStats(user.id)
loadUserActivities(user.id, 1, {
search: '',
startDate: '',
endDate: '',
activityType: 'all'
})
}
// 處理活動記錄篩選
const handleActivityFilter = (newFilters: any) => {
const updatedFilters = { ...activityFilters, ...newFilters }
setActivityFilters(updatedFilters)
if (selectedUser) {
loadUserActivities(selectedUser.id, 1, updatedFilters)
}
}
// 處理活動記錄分頁
const handleActivityPageChange = (page: number) => {
setActivityPagination(prev => ({ ...prev, page }))
if (selectedUser) {
loadUserActivities(selectedUser.id, page, activityFilters)
}
} }
const handleEditUser = (user: any) => { const handleEditUser = (user: any) => {
@@ -155,18 +295,51 @@ export function UserManagement() {
const handleToggleUserStatus = async (userId: string) => { const handleToggleUserStatus = async (userId: string) => {
setIsLoading(true) setIsLoading(true)
// 模擬 API 調用 try {
await new Promise((resolve) => setTimeout(resolve, 1000)) // 找到要切換狀態的用戶
const user = users.find(u => u.id === userId)
if (!user) {
setError("用戶不存在")
setIsLoading(false)
return
}
const newStatus = user.status === "active" ? "inactive" : "active"
const response = await fetch(`/api/admin/users/${userId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: user.name,
department: user.department,
role: user.role,
status: newStatus
})
})
const data = await response.json()
if (data.success) {
// 更新本地用戶列表
setUsers( setUsers(
users.map((user) => users.map((u) =>
user.id === userId ? { ...user, status: user.status === "active" ? "inactive" : "active" } : user, u.id === userId ? { ...u, status: newStatus } : u
), )
) )
setIsLoading(false)
setSuccess("用戶狀態更新成功!") setSuccess("用戶狀態更新成功!")
setTimeout(() => setSuccess(""), 3000) setTimeout(() => setSuccess(""), 3000)
} else {
setError(data.error || "更新用戶狀態失敗")
}
} catch (error) {
console.error('更新用戶狀態錯誤:', error)
setError("更新用戶狀態時發生錯誤")
}
setIsLoading(false)
} }
const handleChangeUserRole = async (userId: string, newRole: string) => { const handleChangeUserRole = async (userId: string, newRole: string) => {
@@ -222,13 +395,13 @@ export function UserManagement() {
if (data.success) { if (data.success) {
const newInvitation = { const newInvitation = {
id: Date.now().toString(), id: data.data.user.id,
name: "", name: data.data.user.name || "",
email: inviteEmail, email: data.data.user.email,
department: "", department: data.data.user.department || "",
role: "", role: data.data.user.role,
status: "invited", status: "invited",
joinDate: "", joinDate: data.data.user.join_date ? new Date(data.data.user.join_date).toLocaleDateString('zh-TW') : "",
lastLogin: "", lastLogin: "",
totalApps: 0, totalApps: 0,
totalReviews: 0, totalReviews: 0,
@@ -327,15 +500,38 @@ export function UserManagement() {
setIsLoading(true) setIsLoading(true)
// 模擬 API 調用 try {
await new Promise((resolve) => setTimeout(resolve, 1500)) const response = await fetch(`/api/admin/users/${editUser.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: editUser.name,
department: editUser.department,
role: editUser.role,
status: editUser.status
})
})
const data = await response.json()
if (data.success) {
// 更新本地用戶列表
setUsers(users.map((user) => (user.id === editUser.id ? { ...user, ...editUser } : user))) setUsers(users.map((user) => (user.id === editUser.id ? { ...user, ...editUser } : user)))
setIsLoading(false)
setShowEditUser(false) setShowEditUser(false)
setSuccess("用戶資料更新成功!") setSuccess("用戶資料更新成功!")
setTimeout(() => setSuccess(""), 3000) setTimeout(() => setSuccess(""), 3000)
} else {
setError(data.error || "更新用戶失敗")
}
} catch (error) {
console.error('更新用戶錯誤:', error)
setError("更新用戶時發生錯誤")
}
setIsLoading(false)
} }
const confirmDeleteUser = async () => { const confirmDeleteUser = async () => {
@@ -343,16 +539,30 @@ export function UserManagement() {
setIsLoading(true) setIsLoading(true)
// 模擬 API 調用 try {
await new Promise((resolve) => setTimeout(resolve, 1500)) const response = await fetch(`/api/admin/users/${userToDelete.id}`, {
method: 'DELETE'
})
const data = await response.json()
if (data.success) {
// 從本地用戶列表中移除
setUsers(users.filter((user) => user.id !== userToDelete.id)) setUsers(users.filter((user) => user.id !== userToDelete.id))
setIsLoading(false)
setShowDeleteConfirm(false) setShowDeleteConfirm(false)
setUserToDelete(null) setUserToDelete(null)
setSuccess("用戶刪除成功!") setSuccess("用戶刪除成功!")
setTimeout(() => setSuccess(""), 3000) setTimeout(() => setSuccess(""), 3000)
} else {
setError(data.error || "刪除用戶失敗")
}
} catch (error) {
console.error('刪除用戶錯誤:', error)
setError("刪除用戶時發生錯誤")
}
setIsLoading(false)
} }
const getRoleColor = (role: string) => { const getRoleColor = (role: string) => {
@@ -407,6 +617,44 @@ export function UserManagement() {
} }
} }
// 獲取活動圖標
const getActivityIcon = (iconName: string) => {
switch (iconName) {
case 'Calendar':
return <Calendar className="w-4 h-4" />
case 'Heart':
return <Heart className="w-4 h-4" />
case 'ThumbsUp':
return <ThumbsUp className="w-4 h-4" />
case 'Eye':
return <Eye className="w-4 h-4" />
case 'Star':
return <Star className="w-4 h-4" />
case 'Plus':
return <Plus className="w-4 h-4" />
default:
return <Calendar className="w-4 h-4" />
}
}
// 獲取活動顏色
const getActivityColor = (color: string) => {
switch (color) {
case 'blue':
return 'text-blue-600'
case 'red':
return 'text-red-600'
case 'green':
return 'text-green-600'
case 'purple':
return 'text-purple-600'
case 'yellow':
return 'text-yellow-600'
default:
return 'text-gray-600'
}
}
const getRoleIcon = (role: string) => { const getRoleIcon = (role: string) => {
switch (role) { switch (role) {
case "admin": case "admin":
@@ -1140,22 +1388,141 @@ export function UserManagement() {
<p className="text-gray-500"></p> <p className="text-gray-500"></p>
</div> </div>
) : ( ) : (
<>
{/* 篩選控件 */}
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-center space-x-3 p-3 bg-gray-50 rounded-lg"> <div className="flex flex-wrap items-end justify-between gap-3">
<Calendar className="w-4 h-4 text-blue-600" /> <div className="flex flex-wrap gap-3 items-end">
<div> <div className="flex-1 min-w-[180px]">
<p className="text-sm font-medium"></p> <Label htmlFor="activity-search" className="text-xs"></Label>
<p className="text-xs text-gray-500">2024-01-20 16:45</p> <Input
id="activity-search"
placeholder="搜尋活動內容..."
value={activityFilters.search}
onChange={(e) => handleActivityFilter({ search: e.target.value })}
className="mt-1 h-8 text-sm"
/>
</div>
<div className="min-w-[100px]">
<Label htmlFor="activity-type" className="text-xs"></Label>
<Select
value={activityFilters.activityType}
onValueChange={(value) => handleActivityFilter({ activityType: value })}
>
<SelectTrigger className="mt-1 h-8 text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="login"></SelectItem>
<SelectItem value="favorite"></SelectItem>
<SelectItem value="like"></SelectItem>
<SelectItem value="view"></SelectItem>
<SelectItem value="rating"></SelectItem>
<SelectItem value="create"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="min-w-[120px]">
<Label htmlFor="start-date" className="text-xs"></Label>
<Input
id="start-date"
type="date"
value={activityFilters.startDate}
onChange={(e) => handleActivityFilter({ startDate: e.target.value })}
className="mt-1 h-8 text-sm"
/>
</div>
<div className="min-w-[120px]">
<Label htmlFor="end-date" className="text-xs"></Label>
<Input
id="end-date"
type="date"
value={activityFilters.endDate}
onChange={(e) => handleActivityFilter({ endDate: e.target.value })}
className="mt-1 h-8 text-sm"
/>
</div> </div>
</div> </div>
<div className="flex items-center space-x-3 p-3 bg-gray-50 rounded-lg"> <Button
<Eye className="w-4 h-4 text-green-600" /> variant="outline"
<div> size="sm"
<p className="text-sm font-medium"></p> onClick={() => handleActivityFilter({
<p className="text-xs text-gray-500">2024-01-20 15:30</p> search: '',
startDate: '',
endDate: '',
activityType: 'all'
})}
className="h-8 px-3 text-xs"
>
<X className="w-3 h-3 mr-1" />
</Button>
</div> </div>
</div> </div>
{/* 活動記錄列表 */}
{userActivities.length === 0 ? (
<div className="text-center py-8">
<Calendar className="w-12 h-12 text-gray-400 mx-auto mb-4" />
<p className="text-gray-500"></p>
</div> </div>
) : (
<>
<div className="space-y-3">
{userActivities.map((activity, index) => (
<div key={index} className="flex items-center space-x-3 p-3 bg-gray-50 rounded-lg">
<div className={`${getActivityColor(activity.color)}`}>
{getActivityIcon(activity.icon)}
</div>
<div className="flex-1">
<p className="text-sm font-medium">{activity.action_text}</p>
<p className="text-xs text-gray-500">
{new Date(activity.created_at).toLocaleString('zh-TW', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
})}
</p>
</div>
</div>
))}
</div>
{/* 分頁控件 */}
{activityPagination.totalPages > 1 && (
<div className="flex items-center justify-between pt-4">
<div className="text-sm text-gray-500">
{((activityPagination.page - 1) * activityPagination.limit) + 1} - {Math.min(activityPagination.page * activityPagination.limit, activityPagination.total)} {activityPagination.total}
</div>
<div className="flex items-center space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => handleActivityPageChange(activityPagination.page - 1)}
disabled={activityPagination.page <= 1}
>
<ChevronLeft className="w-4 h-4" />
</Button>
<span className="text-sm">
{activityPagination.page} {activityPagination.totalPages}
</span>
<Button
variant="outline"
size="sm"
onClick={() => handleActivityPageChange(activityPagination.page + 1)}
disabled={activityPagination.page >= activityPagination.totalPages}
>
<ChevronRight className="w-4 h-4" />
</Button>
</div>
</div>
)}
</>
)}
</>
)} )}
</TabsContent> </TabsContent>
@@ -1164,7 +1531,7 @@ export function UserManagement() {
<Card> <Card>
<CardContent className="p-4"> <CardContent className="p-4">
<div className="text-center"> <div className="text-center">
<p className="text-2xl font-bold text-blue-600">{selectedUser.totalApps}</p> <p className="text-2xl font-bold text-blue-600">{userStats.totalApps}</p>
<p className="text-sm text-gray-600"></p> <p className="text-sm text-gray-600"></p>
</div> </div>
</CardContent> </CardContent>
@@ -1172,7 +1539,7 @@ export function UserManagement() {
<Card> <Card>
<CardContent className="p-4"> <CardContent className="p-4">
<div className="text-center"> <div className="text-center">
<p className="text-2xl font-bold text-green-600">{selectedUser.totalReviews}</p> <p className="text-2xl font-bold text-green-600">{userStats.totalReviews}</p>
<p className="text-sm text-gray-600"></p> <p className="text-sm text-gray-600"></p>
</div> </div>
</CardContent> </CardContent>
@@ -1180,7 +1547,7 @@ export function UserManagement() {
<Card> <Card>
<CardContent className="p-4"> <CardContent className="p-4">
<div className="text-center"> <div className="text-center">
<p className="text-2xl font-bold text-purple-600">{selectedUser.totalLikes}</p> <p className="text-2xl font-bold text-purple-600">{userStats.totalLikes}</p>
<p className="text-sm text-gray-600"></p> <p className="text-sm text-gray-600"></p>
</div> </div>
</CardContent> </CardContent>
@@ -1188,9 +1555,7 @@ export function UserManagement() {
<Card> <Card>
<CardContent className="p-4"> <CardContent className="p-4">
<div className="text-center"> <div className="text-center">
<p className="text-2xl font-bold text-orange-600"> <p className="text-2xl font-bold text-orange-600">{userStats.loginDays}</p>
{selectedUser.status === "invited" ? 0 : 15}
</p>
<p className="text-sm text-gray-600"></p> <p className="text-sm text-gray-600"></p>
</div> </div>
</CardContent> </CardContent>

View File

@@ -61,6 +61,7 @@ interface RegisterData {
email: string email: string
password: string password: string
department: string department: string
role?: string
} }
const AuthContext = createContext<AuthContextType | undefined>(undefined) const AuthContext = createContext<AuthContextType | undefined>(undefined)

View File

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

View File

@@ -3,6 +3,8 @@
// ===================================================== // =====================================================
import { db } from '../database'; import { db } from '../database';
import bcrypt from 'bcryptjs';
import crypto from 'crypto';
import type { import type {
User, User,
Judge, Judge,
@@ -37,7 +39,7 @@ export class UserService {
// 創建用戶 // 創建用戶
async create(userData: Omit<User, 'id' | 'created_at' | 'updated_at'>): Promise<User> { async create(userData: Omit<User, 'id' | 'created_at' | 'updated_at'>): Promise<User> {
const sql = ` 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`; `;
const params = [ const params = [
@@ -51,7 +53,7 @@ export class UserService {
userData.join_date, userData.join_date,
userData.total_likes, userData.total_likes,
userData.total_views, userData.total_views,
userData.is_active, userData.status,
userData.last_login || null userData.last_login || null
]; ];
@@ -61,13 +63,13 @@ export class UserService {
// 根據郵箱獲取用戶 // 根據郵箱獲取用戶
async findByEmail(email: string): Promise<User | null> { 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]); return await db.queryOne<User>(sql, [email]);
} }
// 根據ID獲取用戶 // 根據ID獲取用戶
async findById(id: string): Promise<User | null> { 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]); return await db.queryOne<User>(sql, [id]);
} }
@@ -111,7 +113,7 @@ export class UserService {
const { search, department, role, status, page = 1, limit = 10 } = filters; const { search, department, role, status, page = 1, limit = 10 } = filters;
// 構建查詢條件 // 構建查詢條件
let whereConditions = ['is_active = TRUE']; let whereConditions: string[] = [];
let params: any[] = []; let params: any[] = [];
if (search) { if (search) {
@@ -131,9 +133,14 @@ export class UserService {
if (status && status !== 'all') { if (status && status !== 'all') {
if (status === 'active') { 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') { } 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 = ` const usersSql = `
SELECT SELECT
id, name, email, avatar, department, role, join_date, 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 FROM users
${whereClause} ${whereClause}
ORDER BY created_at DESC ORDER BY created_at DESC
@@ -166,19 +173,20 @@ export class UserService {
activeUsers: number; activeUsers: number;
adminCount: number; adminCount: number;
developerCount: number; developerCount: number;
invitedUsers: number;
inactiveUsers: number; inactiveUsers: number;
newThisMonth: number; newThisMonth: number;
}> { }> {
const sql = ` const sql = `
SELECT SELECT
COUNT(*) as total_users, 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 = 'admin' THEN 1 END) as admin_count,
COUNT(CASE WHEN role = 'developer' THEN 1 END) as developer_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 status = 'invited' THEN 1 END) as invited_users,
COUNT(CASE WHEN created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY) THEN 1 END) as new_this_month 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 FROM users
WHERE is_active = TRUE
`; `;
const result = await this.query(sql); const result = await this.query(sql);
const stats = result[0] || {}; const stats = result[0] || {};
@@ -188,16 +196,544 @@ export class UserService {
activeUsers: stats.active_users || 0, activeUsers: stats.active_users || 0,
adminCount: stats.admin_count || 0, adminCount: stats.admin_count || 0,
developerCount: stats.developer_count || 0, developerCount: stats.developer_count || 0,
invitedUsers: stats.invited_users || 0,
inactiveUsers: stats.inactive_users || 0, inactiveUsers: stats.inactive_users || 0,
newThisMonth: stats.new_this_month || 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[]> { async query<T = any>(sql: string, params: any[] = []): Promise<T[]> {
return await db.query<T>(sql, params); 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> { async queryOne<T = any>(sql: string, params: any[] = []): Promise<T | null> {
return await db.queryOne<T>(sql, params); return await db.queryOne<T>(sql, params);