實作管理者用戶管理、邀請註冊功能
This commit is contained in:
33
app/api/admin/dashboard/route.ts
Normal file
33
app/api/admin/dashboard/route.ts
Normal 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 }
|
||||
)
|
||||
}
|
||||
}
|
46
app/api/admin/users/[id]/activities/route.ts
Normal file
46
app/api/admin/users/[id]/activities/route.ts
Normal 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 }
|
||||
)
|
||||
}
|
||||
}
|
@@ -3,71 +3,71 @@ import { UserService } from '@/lib/services/database-service'
|
||||
|
||||
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(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const updates = await request.json()
|
||||
const { id: userId } = await params
|
||||
const body = await request.json()
|
||||
const { name, department, role, status } = body
|
||||
|
||||
// 移除不允許更新的欄位
|
||||
delete updates.id
|
||||
delete updates.created_at
|
||||
delete updates.password_hash
|
||||
|
||||
const updatedUser = await userService.update(params.id, updates)
|
||||
|
||||
if (!updatedUser) {
|
||||
if (!userId) {
|
||||
return NextResponse.json(
|
||||
{ error: '用戶不存在或更新失敗' },
|
||||
{ status: 404 }
|
||||
{ success: false, error: '用戶 ID 是必需的' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: '用戶資料已更新',
|
||||
data: updatedUser
|
||||
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({
|
||||
success: true,
|
||||
message: '用戶資料更新成功',
|
||||
data: result.user
|
||||
})
|
||||
} else {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: result.error },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('更新用戶錯誤:', error)
|
||||
return NextResponse.json(
|
||||
{ error: '更新用戶時發生錯誤' },
|
||||
{ success: false, error: '更新用戶時發生錯誤' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
@@ -75,28 +75,36 @@ export async function PUT(
|
||||
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
// 軟刪除:將 is_active 設為 false
|
||||
const result = await userService.update(params.id, { is_active: false })
|
||||
const { id: userId } = await params
|
||||
|
||||
if (!result) {
|
||||
if (!userId) {
|
||||
return NextResponse.json(
|
||||
{ error: '用戶不存在或刪除失敗' },
|
||||
{ status: 404 }
|
||||
{ success: false, error: '用戶 ID 是必需的' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: '用戶已刪除'
|
||||
})
|
||||
const result = await userService.deleteUser(userId)
|
||||
|
||||
if (result.success) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: '用戶刪除成功'
|
||||
})
|
||||
} else {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: result.error },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('刪除用戶錯誤:', error)
|
||||
return NextResponse.json(
|
||||
{ error: '刪除用戶時發生錯誤' },
|
||||
{ success: false, error: '刪除用戶時發生錯誤' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
34
app/api/admin/users/[id]/stats/route.ts
Normal file
34
app/api/admin/users/[id]/stats/route.ts
Normal 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 }
|
||||
)
|
||||
}
|
||||
}
|
@@ -3,6 +3,73 @@ import { UserService } from '@/lib/services/database-service'
|
||||
|
||||
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) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
@@ -14,7 +81,7 @@ export async function GET(request: NextRequest) {
|
||||
const status = searchParams.get('status') || ''
|
||||
|
||||
// 構建查詢條件
|
||||
let whereConditions = ['is_active = TRUE']
|
||||
let whereConditions = ['status IN ("active", "inactive", "invited")']
|
||||
let params: any[] = []
|
||||
|
||||
if (search) {
|
||||
@@ -34,9 +101,11 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
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 = "active"')
|
||||
} 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 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({
|
||||
success: true,
|
||||
data: {
|
||||
users,
|
||||
users: mappedUsers,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
@@ -65,12 +145,12 @@ export async function GET(request: NextRequest) {
|
||||
totalPages: Math.ceil(total / limit)
|
||||
},
|
||||
stats: {
|
||||
totalUsers: stats?.total_users || 0,
|
||||
activeUsers: stats?.active_users || 0,
|
||||
adminCount: stats?.admin_count || 0,
|
||||
developerCount: stats?.developer_count || 0,
|
||||
inactiveUsers: stats?.inactive_users || 0,
|
||||
newThisMonth: stats?.new_this_month || 0
|
||||
totalUsers: stats?.totalUsers || 0,
|
||||
activeUsers: stats?.activeUsers || 0,
|
||||
adminCount: stats?.adminCount || 0,
|
||||
developerCount: stats?.developerCount || 0,
|
||||
inactiveUsers: stats?.invitedUsers || 0, // 將 invitedUsers 映射為 inactiveUsers(待註冊)
|
||||
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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@@ -23,42 +23,69 @@ export async function POST(request: NextRequest) {
|
||||
)
|
||||
}
|
||||
|
||||
// 檢查用戶是否已存在
|
||||
const existingUser = await userService.findByEmail(email)
|
||||
if (existingUser) {
|
||||
return NextResponse.json(
|
||||
{ error: '該電子郵件已被註冊' },
|
||||
{ status: 409 }
|
||||
)
|
||||
}
|
||||
|
||||
// 加密密碼
|
||||
const saltRounds = 12
|
||||
const password_hash = await bcrypt.hash(password, saltRounds)
|
||||
|
||||
// 創建新用戶
|
||||
const newUser = {
|
||||
id: uuidv4(),
|
||||
name,
|
||||
email,
|
||||
password_hash,
|
||||
department,
|
||||
role: role as 'user' | 'developer' | 'admin',
|
||||
join_date: new Date().toISOString().split('T')[0],
|
||||
total_likes: 0,
|
||||
total_views: 0,
|
||||
is_active: true
|
||||
// 檢查是否為邀請用戶(狀態為 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)
|
||||
if (existingUser) {
|
||||
return NextResponse.json(
|
||||
{ error: '該電子郵件已被註冊' },
|
||||
{ status: 409 }
|
||||
)
|
||||
}
|
||||
|
||||
// 創建新用戶
|
||||
const newUser = {
|
||||
id: uuidv4(),
|
||||
name,
|
||||
email,
|
||||
password_hash,
|
||||
department,
|
||||
role: role as 'user' | 'developer' | 'admin',
|
||||
join_date: new Date().toISOString().split('T')[0],
|
||||
total_likes: 0,
|
||||
total_views: 0,
|
||||
status: 'active' as 'active' | 'inactive' | 'invited'
|
||||
}
|
||||
|
||||
const createdUser = await userService.create(newUser)
|
||||
|
||||
// 返回用戶信息(不包含密碼)
|
||||
const { password_hash: _, ...userWithoutPassword } = createdUser
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
user: userWithoutPassword
|
||||
})
|
||||
}
|
||||
|
||||
const createdUser = await userService.create(newUser)
|
||||
|
||||
// 返回用戶信息(不包含密碼)
|
||||
const { password_hash: _, ...userWithoutPassword } = createdUser
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
user: userWithoutPassword
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('註冊錯誤:', error)
|
||||
return NextResponse.json(
|
||||
|
@@ -130,6 +130,7 @@ export default function RegisterPage() {
|
||||
email: formData.email,
|
||||
password: formData.password,
|
||||
department: formData.department,
|
||||
role: isInvitedUser ? invitedRole : 'user', // 如果是邀請用戶,使用邀請的角色
|
||||
})
|
||||
|
||||
if (success) {
|
||||
|
@@ -33,9 +33,17 @@ import {
|
||||
RefreshCw,
|
||||
Copy,
|
||||
Link,
|
||||
Heart,
|
||||
ThumbsUp,
|
||||
Star,
|
||||
Plus,
|
||||
ExternalLink,
|
||||
Code,
|
||||
Users,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Filter,
|
||||
X,
|
||||
} from "lucide-react"
|
||||
|
||||
export function UserManagement() {
|
||||
@@ -45,6 +53,25 @@ export function UserManagement() {
|
||||
const [selectedRole, setSelectedRole] = useState("all")
|
||||
const [selectedStatus, setSelectedStatus] = useState("all")
|
||||
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 [showInviteUser, setShowInviteUser] = useState(false)
|
||||
const [showEditUser, setShowEditUser] = useState(false)
|
||||
@@ -71,6 +98,74 @@ export function UserManagement() {
|
||||
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 () => {
|
||||
if (!isClient) return
|
||||
@@ -133,6 +228,51 @@ export function UserManagement() {
|
||||
const handleViewUser = (user: any) => {
|
||||
setSelectedUser(user)
|
||||
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) => {
|
||||
@@ -155,18 +295,51 @@ export function UserManagement() {
|
||||
const handleToggleUserStatus = async (userId: string) => {
|
||||
setIsLoading(true)
|
||||
|
||||
// 模擬 API 調用
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
try {
|
||||
// 找到要切換狀態的用戶
|
||||
const user = users.find(u => u.id === userId)
|
||||
if (!user) {
|
||||
setError("用戶不存在")
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
setUsers(
|
||||
users.map((user) =>
|
||||
user.id === userId ? { ...user, status: user.status === "active" ? "inactive" : "active" } : user,
|
||||
),
|
||||
)
|
||||
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(
|
||||
users.map((u) =>
|
||||
u.id === userId ? { ...u, status: newStatus } : u
|
||||
)
|
||||
)
|
||||
|
||||
setSuccess("用戶狀態更新成功!")
|
||||
setTimeout(() => setSuccess(""), 3000)
|
||||
} else {
|
||||
setError(data.error || "更新用戶狀態失敗")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('更新用戶狀態錯誤:', error)
|
||||
setError("更新用戶狀態時發生錯誤")
|
||||
}
|
||||
|
||||
setIsLoading(false)
|
||||
setSuccess("用戶狀態更新成功!")
|
||||
setTimeout(() => setSuccess(""), 3000)
|
||||
}
|
||||
|
||||
const handleChangeUserRole = async (userId: string, newRole: string) => {
|
||||
@@ -222,13 +395,13 @@ export function UserManagement() {
|
||||
|
||||
if (data.success) {
|
||||
const newInvitation = {
|
||||
id: Date.now().toString(),
|
||||
name: "",
|
||||
email: inviteEmail,
|
||||
department: "",
|
||||
role: "",
|
||||
id: data.data.user.id,
|
||||
name: data.data.user.name || "",
|
||||
email: data.data.user.email,
|
||||
department: data.data.user.department || "",
|
||||
role: data.data.user.role,
|
||||
status: "invited",
|
||||
joinDate: "",
|
||||
joinDate: data.data.user.join_date ? new Date(data.data.user.join_date).toLocaleDateString('zh-TW') : "",
|
||||
lastLogin: "",
|
||||
totalApps: 0,
|
||||
totalReviews: 0,
|
||||
@@ -327,15 +500,38 @@ export function UserManagement() {
|
||||
|
||||
setIsLoading(true)
|
||||
|
||||
// 模擬 API 調用
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500))
|
||||
try {
|
||||
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
|
||||
})
|
||||
})
|
||||
|
||||
setUsers(users.map((user) => (user.id === editUser.id ? { ...user, ...editUser } : user)))
|
||||
const data = await response.json()
|
||||
|
||||
if (data.success) {
|
||||
// 更新本地用戶列表
|
||||
setUsers(users.map((user) => (user.id === editUser.id ? { ...user, ...editUser } : user)))
|
||||
|
||||
setShowEditUser(false)
|
||||
setSuccess("用戶資料更新成功!")
|
||||
setTimeout(() => setSuccess(""), 3000)
|
||||
} else {
|
||||
setError(data.error || "更新用戶失敗")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('更新用戶錯誤:', error)
|
||||
setError("更新用戶時發生錯誤")
|
||||
}
|
||||
|
||||
setIsLoading(false)
|
||||
setShowEditUser(false)
|
||||
setSuccess("用戶資料更新成功!")
|
||||
setTimeout(() => setSuccess(""), 3000)
|
||||
}
|
||||
|
||||
const confirmDeleteUser = async () => {
|
||||
@@ -343,16 +539,30 @@ export function UserManagement() {
|
||||
|
||||
setIsLoading(true)
|
||||
|
||||
// 模擬 API 調用
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500))
|
||||
try {
|
||||
const response = await fetch(`/api/admin/users/${userToDelete.id}`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
|
||||
setUsers(users.filter((user) => user.id !== userToDelete.id))
|
||||
const data = await response.json()
|
||||
|
||||
if (data.success) {
|
||||
// 從本地用戶列表中移除
|
||||
setUsers(users.filter((user) => user.id !== userToDelete.id))
|
||||
|
||||
setShowDeleteConfirm(false)
|
||||
setUserToDelete(null)
|
||||
setSuccess("用戶刪除成功!")
|
||||
setTimeout(() => setSuccess(""), 3000)
|
||||
} else {
|
||||
setError(data.error || "刪除用戶失敗")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('刪除用戶錯誤:', error)
|
||||
setError("刪除用戶時發生錯誤")
|
||||
}
|
||||
|
||||
setIsLoading(false)
|
||||
setShowDeleteConfirm(false)
|
||||
setUserToDelete(null)
|
||||
setSuccess("用戶刪除成功!")
|
||||
setTimeout(() => setSuccess(""), 3000)
|
||||
}
|
||||
|
||||
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) => {
|
||||
switch (role) {
|
||||
case "admin":
|
||||
@@ -1140,22 +1388,141 @@ export function UserManagement() {
|
||||
<p className="text-gray-500">用戶尚未註冊,暫無活動記錄</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center space-x-3 p-3 bg-gray-50 rounded-lg">
|
||||
<Calendar className="w-4 h-4 text-blue-600" />
|
||||
<div>
|
||||
<p className="text-sm font-medium">登入系統</p>
|
||||
<p className="text-xs text-gray-500">2024-01-20 16:45</p>
|
||||
<>
|
||||
{/* 篩選控件 */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex flex-wrap items-end justify-between gap-3">
|
||||
<div className="flex flex-wrap gap-3 items-end">
|
||||
<div className="flex-1 min-w-[180px]">
|
||||
<Label htmlFor="activity-search" className="text-xs">搜尋活動</Label>
|
||||
<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>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleActivityFilter({
|
||||
search: '',
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
activityType: 'all'
|
||||
})}
|
||||
className="h-8 px-3 text-xs"
|
||||
>
|
||||
<X className="w-3 h-3 mr-1" />
|
||||
清除
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3 p-3 bg-gray-50 rounded-lg">
|
||||
<Eye className="w-4 h-4 text-green-600" />
|
||||
<div>
|
||||
<p className="text-sm font-medium">查看應用:智能對話助手</p>
|
||||
<p className="text-xs text-gray-500">2024-01-20 15:30</p>
|
||||
|
||||
{/* 活動記錄列表 */}
|
||||
{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>
|
||||
) : (
|
||||
<>
|
||||
<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>
|
||||
|
||||
@@ -1164,7 +1531,7 @@ export function UserManagement() {
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<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>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -1172,7 +1539,7 @@ export function UserManagement() {
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<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>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -1180,7 +1547,7 @@ export function UserManagement() {
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<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>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -1188,9 +1555,7 @@ export function UserManagement() {
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="text-center">
|
||||
<p className="text-2xl font-bold text-orange-600">
|
||||
{selectedUser.status === "invited" ? 0 : 15}
|
||||
</p>
|
||||
<p className="text-2xl font-bold text-orange-600">{userStats.loginDays}</p>
|
||||
<p className="text-sm text-gray-600">登入天數</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
@@ -61,6 +61,7 @@ interface RegisterData {
|
||||
email: string
|
||||
password: string
|
||||
department: string
|
||||
role?: string
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined)
|
||||
|
@@ -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;
|
||||
|
@@ -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
|
||||
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
|
||||
WHERE is_active = TRUE
|
||||
`;
|
||||
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