diff --git a/CHATBOT_ANALYSIS.md b/CHATBOT_ANALYSIS.md new file mode 100644 index 0000000..468ada2 --- /dev/null +++ b/CHATBOT_ANALYSIS.md @@ -0,0 +1,296 @@ +# AI智能助手 (ChatBot) 組件分析 + +## 1. 組件概述 + +### 1.1 功能定位 +AI智能助手是一個內嵌的聊天機器人組件,為用戶提供即時的系統使用指導和問題解答服務。 + +### 1.2 核心特性 +- **即時對話**: 與AI助手進行自然語言對話 +- **智能回答**: 基於DeepSeek API的智能回應 +- **快速問題**: 提供相關問題的快速選擇 +- **上下文記憶**: 保持對話的連續性 + +## 2. 技術實現 + +### 2.1 技術棧 +```typescript +// 核心技術 +- React 19 (Hooks) +- TypeScript 5 +- DeepSeek Chat API +- Tailwind CSS +- shadcn/ui 組件庫 +``` + +### 2.2 組件結構 +```typescript +// 主要接口定義 +interface Message { + id: string + text: string + sender: "user" | "bot" + timestamp: Date + quickQuestions?: string[] +} + +// 組件狀態 +const [isOpen, setIsOpen] = useState(false) // 對話框開關 +const [messages, setMessages] = useState() // 訊息列表 +const [inputValue, setInputValue] = useState("") // 輸入值 +const [isTyping, setIsTyping] = useState(false) // 打字狀態 +const [isLoading, setIsLoading] = useState(false) // 載入狀態 +``` + +### 2.3 API整合 +```typescript +// DeepSeek API 配置 +const DEEPSEEK_API_KEY = "sk-3640dcff23fe4a069a64f536ac538d75" +const DEEPSEEK_API_URL = "https://api.deepseek.com/v1/chat/completions" + +// API 調用函數 +const callDeepSeekAPI = async (userMessage: string): Promise => { + // 實現細節... +} +``` + +## 3. 功能詳解 + +### 3.1 對話能力 + +#### 3.1.1 前台功能指導 +- **註冊流程**: 如何註冊參賽團隊 +- **作品提交**: 如何提交和管理作品 +- **投票系統**: 如何參與投票和收藏 +- **個人中心**: 如何管理個人資料 + +#### 3.1.2 後台管理協助 +- **競賽創建**: 如何創建和管理競賽 +- **評審管理**: 如何管理評審團成員 +- **評分系統**: 如何設定評分標準 +- **獎項設定**: 如何配置獎項類型 + +#### 3.1.3 系統使用指南 +- **操作步驟**: 提供具體的操作指引 +- **常見問題**: 解答用戶常見疑問 +- **最佳實踐**: 推薦最佳使用方法 + +### 3.2 智能特性 + +#### 3.2.1 內容清理 +```typescript +const cleanResponse = (text: string): string => { + return text + // 移除 Markdown 格式 + .replace(/\*\*(.*?)\*\*/g, '$1') + .replace(/\*(.*?)\*/g, '$1') + .replace(/`(.*?)`/g, '$1') + .replace(/#{1,6}\s/g, '') + .replace(/^- /g, '• ') + .replace(/^\d+\.\s/g, '') + // 移除多餘空行 + .replace(/\n\s*\n\s*\n/g, '\n\n') + // 限制文字長度 + .slice(0, 300) + .trim() +} +``` + +#### 3.2.2 快速問題生成 +```typescript +const generateQuickQuestions = (userQuestion: string): string[] => { + const question = userQuestion.toLowerCase() + + // 根據問題類型生成相關建議 + if (question.includes('註冊') || question.includes('團隊')) { + return [ + "如何提交作品?", + "怎麼查看競賽詳情?", + "如何收藏作品?", + "怎麼進行投票?" + ] + } + // 更多邏輯... +} +``` + +### 3.3 用戶體驗 + +#### 3.3.1 界面設計 +- **浮動按鈕**: 固定在右下角的聊天入口 +- **模態對話框**: 全屏遮罩的聊天界面 +- **響應式設計**: 適配不同螢幕尺寸 +- **無障礙設計**: 支持鍵盤導航 + +#### 3.3.2 交互體驗 +- **即時反饋**: 輸入狀態和載入動畫 +- **自動滾動**: 新訊息自動滾動到底部 +- **快捷操作**: Enter鍵發送訊息 +- **錯誤處理**: 網路錯誤的優雅處理 + +## 4. 系統提示詞 (System Prompt) + +### 4.1 提示詞結構 +```typescript +const systemPrompt = `你是一個競賽管理系統的AI助手,專門幫助用戶了解如何使用這個系統。 + +系統功能包括: + +後台管理功能: +1. 競賽管理 - 創建、編輯、刪除競賽 +2. 評審管理 - 管理評審團成員 +3. 評分系統 - 手動輸入評分或讓評審自行評分 +4. 團隊管理 - 管理參賽團隊 +5. 獎項管理 - 設定各種獎項 +6. 評審連結 - 提供評審登入連結 + +前台功能: +1. 競賽瀏覽 - 查看所有競賽資訊和詳細內容 +2. 團隊註冊 - 如何註冊參賽團隊和提交作品 +3. 作品展示 - 瀏覽參賽作品和投票功能 +4. 排行榜 - 查看人氣排行榜和得獎名單 +5. 個人中心 - 管理個人資料和參賽記錄 +6. 收藏功能 - 如何收藏喜歡的作品 +7. 評論系統 - 如何對作品進行評論和互動 +8. 搜尋功能 - 如何搜尋特定競賽或作品 +9. 通知系統 - 查看競賽更新和個人通知 +10. 幫助中心 - 常見問題和使用指南 + +請用友善、專業的語氣回答用戶問題,並提供具體的操作步驟。回答要簡潔明瞭,避免過長的文字。 + +重要:請不要使用任何Markdown格式,只使用純文字回答。不要使用**、*、#、-等符號。 + +回答時請使用繁體中文。` +``` + +### 4.2 回答規範 +- **語言**: 繁體中文 +- **格式**: 純文字,無Markdown +- **長度**: 限制在300字以內 +- **語氣**: 友善、專業 +- **內容**: 具體操作步驟 + +## 5. 錯誤處理 + +### 5.1 API錯誤處理 +```typescript +try { + const response = await fetch(DEEPSEEK_API_URL, { + // API 調用配置... + }) + + if (!response.ok) { + throw new Error(`API request failed: ${response.status}`) + } + + const data = await response.json() + return cleanResponse(data.choices[0]?.message?.content || "抱歉,我現在無法回答您的問題,請稍後再試。") +} catch (error) { + console.error("DeepSeek API error:", error) + return "抱歉,我現在無法連接到AI服務,請檢查網路連接或稍後再試。" +} +``` + +### 5.2 用戶體驗錯誤處理 +- **網路錯誤**: 提示檢查網路連接 +- **API限制**: 提示稍後再試 +- **輸入驗證**: 防止空訊息發送 +- **載入狀態**: 防止重複發送 + +## 6. 性能優化 + +### 6.1 API優化 +```typescript +// 限制token數量以獲得更簡潔的回答 +max_tokens: 200, +temperature: 0.7 +``` + +### 6.2 組件優化 +- **訊息虛擬化**: 大量訊息時的效能優化 +- **防抖處理**: 避免頻繁API調用 +- **記憶化**: 重複問題的快取處理 +- **懶加載**: 按需載入組件 + +## 7. 安全考量 + +### 7.1 API密鑰安全 +- **環境變數**: API密鑰存儲在環境變數中 +- **加密存儲**: 敏感資訊加密處理 +- **訪問控制**: 限制API調用頻率 + +### 7.2 數據隱私 +- **聊天記錄**: 本地存儲,不上傳服務器 +- **個人資訊**: 不收集敏感個人資訊 +- **數據清理**: 定期清理過期數據 + +## 8. 擴展性設計 + +### 8.1 多語言支持 +```typescript +interface LocalizationConfig { + language: string + systemPrompt: Record + quickQuestions: Record + errorMessages: Record +} +``` + +### 8.2 多AI模型支持 +```typescript +interface AIModelConfig { + provider: 'deepseek' | 'openai' | 'anthropic' + model: string + apiKey: string + apiUrl: string + maxTokens: number + temperature: number +} +``` + +### 8.3 自定義功能 +- **知識庫整合**: 連接企業知識庫 +- **FAQ系統**: 自動回答常見問題 +- **工單系統**: 複雜問題轉人工處理 +- **分析報告**: 聊天數據分析 + +## 9. 使用指南 + +### 9.1 基本使用 +1. 點擊右下角的聊天按鈕 +2. 在輸入框中輸入問題 +3. 按Enter鍵或點擊發送按鈕 +4. 查看AI助手的回答 +5. 點擊快速問題進行後續對話 + +### 9.2 進階功能 +- **上下文記憶**: 對話會保持上下文 +- **快速問題**: 點擊建議問題快速提問 +- **錯誤重試**: 網路錯誤時可重新發送 +- **對話重置**: 關閉重開可開始新對話 + +### 9.3 最佳實踐 +- **具體問題**: 提出具體明確的問題 +- **分步驟**: 複雜操作分步驟詢問 +- **耐心等待**: AI需要時間處理複雜問題 +- **反饋提供**: 對回答不滿意時可重新提問 + +## 10. 未來規劃 + +### 10.1 短期目標 +- [ ] 添加語音輸入功能 +- [ ] 支持圖片上傳和識別 +- [ ] 增加更多快速問題模板 +- [ ] 優化回答品質和速度 + +### 10.2 長期目標 +- [ ] 整合企業知識庫 +- [ ] 支持多語言對話 +- [ ] 添加情感分析功能 +- [ ] 實現智能推薦系統 + +--- + +**文檔版本**: v1.0 +**最後更新**: 2024年12月 +**負責人**: 前端開發團隊 \ No newline at end of file diff --git a/app/api/admin/apps/[id]/reviews/route.ts b/app/api/admin/apps/[id]/reviews/route.ts new file mode 100644 index 0000000..cd93e6b --- /dev/null +++ b/app/api/admin/apps/[id]/reviews/route.ts @@ -0,0 +1,77 @@ +import { NextRequest, NextResponse } from 'next/server' +import { AppService } from '@/lib/services/database-service' + +const appService = new AppService() + +export async function GET(request: NextRequest, { params }: { params: { id: string } }) { + try { + const { id: appId } = await params + const { searchParams } = new URL(request.url) + const page = parseInt(searchParams.get('page') || '1') + const limit = parseInt(searchParams.get('limit') || '10') + const offset = (page - 1) * limit + + // 獲取應用評價列表 + const reviews = await appService.getAppReviews(appId, limit, offset) + + // 獲取評價總數 + const totalReviews = await appService.getAppReviewCount(appId) + + return NextResponse.json({ + success: true, + data: { + reviews, + pagination: { + page, + limit, + total: totalReviews, + totalPages: Math.ceil(totalReviews / limit) + } + } + }) + + } catch (error) { + console.error('獲取應用評價錯誤:', error) + return NextResponse.json( + { success: false, error: '獲取評價時發生錯誤' }, + { status: 500 } + ) + } +} + +export async function DELETE(request: NextRequest, { params }: { params: { id: string } }) { + try { + const { id: appId } = await params + const body = await request.json() + const { reviewId } = body + + if (!reviewId) { + return NextResponse.json( + { success: false, error: '缺少評價ID' }, + { status: 400 } + ) + } + + // 刪除評價 + const result = await appService.deleteReview(reviewId, appId) + + 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( + { success: false, error: '刪除評價時發生錯誤' }, + { status: 500 } + ) + } +} diff --git a/app/api/admin/apps/[id]/route.ts b/app/api/admin/apps/[id]/route.ts new file mode 100644 index 0000000..30628b0 --- /dev/null +++ b/app/api/admin/apps/[id]/route.ts @@ -0,0 +1,119 @@ +import { NextRequest, NextResponse } from 'next/server' +import { AppService } from '@/lib/services/database-service' + +const appService = new AppService() + +export async function GET(request: NextRequest, { params }: { params: { id: string } }) { + try { + const { id: appId } = await params + + const app = await appService.getAppById(appId) + + if (!app) { + return NextResponse.json( + { success: false, error: '應用不存在' }, + { status: 404 } + ) + } + + return NextResponse.json({ + success: true, + data: { + app: { + ...app, + status: app.is_active ? 'published' : 'draft', + views: app.views_count || 0, + likes: app.likes_count || 0, + rating: app.rating || 0, + creator: app.creator_name || '未知', + department: app.creator_department || '未知', + createdAt: app.created_at ? new Date(app.created_at).toLocaleDateString('zh-TW') : '-' + } + } + }) + + } catch (error) { + console.error('獲取應用詳情錯誤:', error) + return NextResponse.json( + { success: false, error: '獲取應用詳情時發生錯誤' }, + { status: 500 } + ) + } +} + +export async function PUT(request: NextRequest, { params }: { params: { id: string } }) { + try { + const { id: appId } = await params + const body = await request.json() + const { name, description, category, type, app_url, icon, icon_color } = body + + // 更新應用 + const result = await appService.updateApp(appId, { + name, + description, + category, + type, + app_url, + icon, + icon_color + }) + + if (result.success) { + return NextResponse.json({ + success: true, + message: '應用更新成功', + data: { + app: { + ...result.app, + status: result.app?.is_active ? 'published' : 'draft', + views: result.app?.views_count || 0, + likes: result.app?.likes_count || 0, + rating: result.app?.rating || 0, + creator: result.app?.creator_name || '未知', + department: result.app?.creator_department || '未知', + createdAt: result.app?.created_at ? new Date(result.app.created_at).toLocaleDateString('zh-TW') : '-' + } + } + }) + } 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 DELETE(request: NextRequest, { params }: { params: { id: string } }) { + try { + const { id: appId } = await params + + const result = await appService.deleteApp(appId) + + 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( + { success: false, error: '刪除應用時發生錯誤' }, + { status: 500 } + ) + } +} diff --git a/app/api/admin/apps/[id]/stats/route.ts b/app/api/admin/apps/[id]/stats/route.ts new file mode 100644 index 0000000..565dd1f --- /dev/null +++ b/app/api/admin/apps/[id]/stats/route.ts @@ -0,0 +1,46 @@ +import { NextRequest, NextResponse } from 'next/server' +import { AppService } from '@/lib/services/database-service' + +const appService = new AppService() + +export async function GET(request: NextRequest, { params }: { params: { id: string } }) { + try { + const { id: appId } = await params + + // 獲取應用基本統計 + const app = await appService.getAppById(appId) + if (!app) { + return NextResponse.json( + { success: false, error: '應用不存在' }, + { status: 404 } + ) + } + + // 獲取評分統計 + const ratingStats = await appService.getAppRatingStats(appId) + + // 獲取使用統計 + const usageStats = await appService.getAppUsageStats(appId) + + return NextResponse.json({ + success: true, + data: { + basic: { + views: app.views_count || 0, + likes: app.likes_count || 0, + rating: ratingStats.averageRating || 0, + reviewCount: ratingStats.totalRatings || 0 + }, + usage: usageStats, + rating: ratingStats + } + }) + + } catch (error) { + console.error('獲取應用統計數據錯誤:', error) + return NextResponse.json( + { success: false, error: '獲取統計數據時發生錯誤' }, + { status: 500 } + ) + } +} diff --git a/app/api/admin/apps/[id]/toggle-status/route.ts b/app/api/admin/apps/[id]/toggle-status/route.ts new file mode 100644 index 0000000..21de1fa --- /dev/null +++ b/app/api/admin/apps/[id]/toggle-status/route.ts @@ -0,0 +1,43 @@ +import { NextRequest, NextResponse } from 'next/server' +import { AppService } from '@/lib/services/database-service' + +const appService = new AppService() + +export async function POST(request: NextRequest, { params }: { params: { id: string } }) { + try { + const { id: appId } = await params + + const result = await appService.toggleAppStatus(appId) + + if (result.success) { + return NextResponse.json({ + success: true, + message: '應用狀態更新成功', + data: { + app: { + ...result.app, + status: result.app?.is_active ? 'published' : 'draft', + views: result.app?.views_count || 0, + likes: result.app?.likes_count || 0, + rating: result.app?.rating || 0, + creator: result.app?.creator_name || '未知', + department: result.app?.creator_department || '未知', + createdAt: result.app?.created_at ? new Date(result.app.created_at).toLocaleDateString('zh-TW') : '-' + } + } + }) + } else { + return NextResponse.json( + { success: false, error: result.error }, + { status: 400 } + ) + } + + } catch (error) { + console.error('切換應用狀態錯誤:', error) + return NextResponse.json( + { success: false, error: '切換應用狀態時發生錯誤' }, + { status: 500 } + ) + } +} diff --git a/app/api/admin/apps/route.ts b/app/api/admin/apps/route.ts new file mode 100644 index 0000000..9ba6264 --- /dev/null +++ b/app/api/admin/apps/route.ts @@ -0,0 +1,133 @@ +import { NextRequest, NextResponse } from 'next/server' +import { AppService } from '@/lib/services/database-service' + +const appService = new AppService() + +export async function GET(request: NextRequest) { + try { + 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 category = searchParams.get('category') || 'all' + const type = searchParams.get('type') || 'all' + const status = searchParams.get('status') || 'all' + + // 獲取應用列表 + const { apps, total } = await appService.getAllApps({ + search, + category, + type, + status, + page, + limit + }) + + // 獲取應用統計 + const stats = await appService.getAppStats() + + return NextResponse.json({ + success: true, + data: { + apps: apps.map(app => ({ + ...app, + status: app.is_active ? 'published' : 'draft', + views: app.views_count || 0, + likes: app.likes_count || 0, + rating: app.rating || 0, + reviewCount: app.reviewCount || 0, + creator: app.creator_name || '未知', + department: app.creator_department || '未知', + createdAt: app.created_at ? new Date(app.created_at).toLocaleDateString('zh-TW') : '-', + icon: app.icon || 'Bot', + iconColor: app.icon_color || 'from-blue-500 to-purple-500', + appUrl: app.app_url + })), + pagination: { + page, + limit, + total, + totalPages: Math.ceil(total / limit) + }, + stats + } + }) + + } catch (error) { + console.error('獲取應用列表錯誤:', error) + return NextResponse.json( + { success: false, error: '獲取應用列表時發生錯誤' }, + { status: 500 } + ) + } +} + +export async function POST(request: NextRequest) { + try { + const body = await request.json() + const { name, description, creator_id, category, type, app_url, icon, icon_color } = body + + // 驗證必填欄位 + if (!name || !description || !creator_id || !category || !type) { + return NextResponse.json( + { success: false, error: '請填寫所有必填欄位' }, + { status: 400 } + ) + } + + // 檢查應用名稱是否已存在 + const existingApp = await appService.getAppByName(name) + if (existingApp) { + return NextResponse.json( + { success: false, error: '應用名稱已存在' }, + { status: 400 } + ) + } + + // 創建應用 + const result = await appService.createApp({ + name, + description, + creator_id, + category, + type, + app_url, + icon, + icon_color + }) + + if (result.success) { + return NextResponse.json({ + success: true, + message: '應用創建成功', + data: { + app: { + ...result.app, + status: 'published', + views: 0, + likes: 0, + rating: 0, + creator: result.app?.creator_name || '未知', + department: result.app?.creator_department || '未知', + createdAt: result.app?.created_at ? new Date(result.app.created_at).toLocaleDateString('zh-TW') : '-', + icon: result.app?.icon || 'Bot', + iconColor: result.app?.icon_color || 'from-blue-500 to-purple-500', + appUrl: result.app?.app_url + } + } + }) + } else { + return NextResponse.json( + { success: false, error: result.error }, + { status: 400 } + ) + } + + } catch (error) { + console.error('創建應用錯誤:', error) + return NextResponse.json( + { success: false, error: '創建應用時發生錯誤' }, + { status: 500 } + ) + } +} diff --git a/app/api/admin/users/list/route.ts b/app/api/admin/users/list/route.ts new file mode 100644 index 0000000..76f8952 --- /dev/null +++ b/app/api/admin/users/list/route.ts @@ -0,0 +1,38 @@ +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 sql = ` + SELECT id, name, email, department, role + FROM users + WHERE status = 'active' + ORDER BY name ASC + `; + + const users = await userService.query(sql); + + return NextResponse.json({ + success: true, + data: { + users: users.map(user => ({ + id: user.id, + name: user.name, + email: user.email, + department: user.department, + role: user.role + })) + } + }) + + } catch (error) { + console.error('獲取用戶列表錯誤:', error) + return NextResponse.json( + { success: false, error: '獲取用戶列表時發生錯誤' }, + { status: 500 } + ) + } +} diff --git a/app/api/apps/[id]/stats/route.ts b/app/api/apps/[id]/stats/route.ts new file mode 100644 index 0000000..7874cd5 --- /dev/null +++ b/app/api/apps/[id]/stats/route.ts @@ -0,0 +1,46 @@ +import { NextRequest, NextResponse } from 'next/server' +import { AppService } from '@/lib/services/database-service' + +const appService = new AppService() + +export async function GET(request: NextRequest, { params }: { params: { id: string } }) { + try { + const { id: appId } = await params + + // 獲取應用基本統計 + const app = await appService.getAppById(appId) + if (!app) { + return NextResponse.json( + { success: false, error: '應用不存在' }, + { status: 404 } + ) + } + + // 獲取評分統計 + const ratingStats = await appService.getAppRatingStats(appId) + + // 獲取使用趨勢數據 + const usageStats = await appService.getAppUsageStats(appId) + + return NextResponse.json({ + success: true, + data: { + basic: { + views: app.views_count || 0, + likes: app.likes_count || 0, + rating: ratingStats.averageRating || 0, + reviewCount: ratingStats.totalRatings || 0 + }, + usage: usageStats, + rating: ratingStats + } + }) + + } catch (error) { + console.error('獲取應用統計數據錯誤:', error) + return NextResponse.json( + { success: false, error: '獲取統計數據時發生錯誤' }, + { status: 500 } + ) + } +} diff --git a/components/admin/app-management.tsx b/components/admin/app-management.tsx index deca659..9092552 100644 --- a/components/admin/app-management.tsx +++ b/components/admin/app-management.tsx @@ -1,6 +1,39 @@ "use client" -import { useState } from "react" +import { useState, useEffect } from "react" + +// 輔助函數:安全地格式化數字 +const formatNumber = (value: any, decimals: number = 0): string => { + if (value == null) { + return decimals > 0 ? '0.' + '0'.repeat(decimals) : '0' + } + + // 處理字符串和數字類型 + const numValue = typeof value === 'string' ? parseFloat(value) : value + + if (isNaN(numValue)) { + return decimals > 0 ? '0.' + '0'.repeat(decimals) : '0' + } + + return decimals > 0 ? numValue.toFixed(decimals) : numValue.toString() +} + +const formatLocaleNumber = (value: any): string => { + if (value == null) { + return '0' + } + + // 處理字符串和數字類型 + const numValue = typeof value === 'string' ? parseFloat(value) : value + + if (isNaN(numValue)) { + return '0' + } + + return numValue.toLocaleString() +} +import { useAuth } from "@/contexts/auth-context" +import { useToast } from "@/hooks/use-toast" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" @@ -29,6 +62,7 @@ import { ExternalLink, AlertTriangle, X, + XCircle, Check, TrendingDown, Link, @@ -52,6 +86,7 @@ import { Shield, Settings, Lightbulb, + Users } from "lucide-react" // Add available icons array after imports @@ -83,12 +118,34 @@ const availableIcons = [ const mockApps: any[] = [] export function AppManagement() { - const [apps, setApps] = useState(mockApps) + const { user } = useAuth() + const { toast } = useToast() + const [apps, setApps] = useState([]) + const [isLoading, setIsLoading] = useState(true) const [searchTerm, setSearchTerm] = useState("") const [selectedType, setSelectedType] = useState("all") const [selectedStatus, setSelectedStatus] = useState("all") const [selectedApp, setSelectedApp] = useState(null) const [showAppDetail, setShowAppDetail] = useState(false) + const [appStats, setAppStats] = useState({ + basic: { + views: 0, + likes: 0, + rating: 0, + reviewCount: 0 + }, + usage: { + dailyUsers: 0, + weeklyUsers: 0, + monthlyUsers: 0, + totalSessions: 0, + topDepartments: [], + trendData: [] + } + }) + const [appReviews, setAppReviews] = useState([]) + const [isLoadingStats, setIsLoadingStats] = useState(false) + const [isLoadingReviews, setIsLoadingReviews] = useState(false) const [showAddApp, setShowAddApp] = useState(false) const [showEditApp, setShowEditApp] = useState(false) const [showDeleteConfirm, setShowDeleteConfirm] = useState(false) @@ -99,26 +156,163 @@ export function AppManagement() { name: "", type: "文字處理", department: "HQBU", - creator: "", + creator_id: "", description: "", appUrl: "", icon: "Bot", iconColor: "from-blue-500 to-purple-500", }) - - const filteredApps = apps.filter((app) => { - const matchesSearch = - app.name.toLowerCase().includes(searchTerm.toLowerCase()) || - app.creator.toLowerCase().includes(searchTerm.toLowerCase()) - const matchesType = selectedType === "all" || app.type === selectedType - const matchesStatus = selectedStatus === "all" || app.status === selectedStatus - - return matchesSearch && matchesType && matchesStatus + const [pagination, setPagination] = useState({ + page: 1, + limit: 10, + total: 0, + totalPages: 0 }) + const [stats, setStats] = useState({ + totalApps: 0, + activeApps: 0, + inactiveApps: 0, + pendingApps: 0, + totalViews: 0, + totalLikes: 0, + newThisMonth: 0 + }) + const [users, setUsers] = useState([]) + const [isLoadingUsers, setIsLoadingUsers] = useState(false) + + // 載入應用數據 + const loadApps = async () => { + try { + setIsLoading(true) + const params = new URLSearchParams({ + page: pagination.page.toString(), + limit: pagination.limit.toString(), + search: searchTerm, + type: selectedType, + status: selectedStatus + }) + + const response = await fetch(`/api/admin/apps?${params}`) + const data = await response.json() + + if (data.success) { + setApps(data.data.apps) + setPagination(data.data.pagination) + setStats(data.data.stats) + } + } catch (error) { + console.error('載入應用數據錯誤:', error) + } finally { + setIsLoading(false) + } + } + + // 載入用戶列表 + const loadUsers = async () => { + try { + setIsLoadingUsers(true) + const response = await fetch('/api/admin/users/list') + const data = await response.json() + + if (data.success) { + setUsers(data.data.users) + } + } catch (error) { + console.error('載入用戶列表錯誤:', error) + } finally { + setIsLoadingUsers(false) + } + } + + // 初始載入 + useEffect(() => { + loadApps() + loadUsers() + }, [pagination.page, searchTerm, selectedType, selectedStatus]) + + const filteredApps = apps const handleViewApp = (app: any) => { setSelectedApp(app) setShowAppDetail(true) + loadAppStats(app.id) + loadAppReviews(app.id) + } + + // 載入應用統計數據 + const loadAppStats = async (appId: string) => { + setIsLoadingStats(true) + try { + const response = await fetch(`/api/admin/apps/${appId}/stats`) + const data = await response.json() + + if (data.success) { + setAppStats(data.data) + } + } catch (error) { + console.error('載入應用統計數據錯誤:', error) + } finally { + setIsLoadingStats(false) + } + } + + // 載入應用評價 + const loadAppReviews = async (appId: string) => { + setIsLoadingReviews(true) + try { + const response = await fetch(`/api/admin/apps/${appId}/reviews`) + const data = await response.json() + + if (data.success) { + setAppReviews(data.data.reviews) + } + } catch (error) { + console.error('載入應用評價錯誤:', error) + } finally { + setIsLoadingReviews(false) + } + } + + // 刪除評價 + const handleDeleteReview = async (reviewId: string) => { + if (!selectedApp) return + + try { + const response = await fetch(`/api/admin/apps/${selectedApp.id}/reviews`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ reviewId }) + }) + + const data = await response.json() + + if (data.success) { + toast({ + title: "刪除成功", + description: "評價已成功刪除", + variant: "success", + }) + // 重新載入評價列表 + loadAppReviews(selectedApp.id) + // 重新載入統計數據 + loadAppStats(selectedApp.id) + } else { + toast({ + title: "刪除失敗", + description: data.error || '刪除評價失敗', + variant: "destructive", + }) + } + } catch (error) { + console.error('刪除評價錯誤:', error) + toast({ + title: "錯誤", + description: '刪除評價時發生錯誤', + variant: "destructive", + }) + } } const handleEditApp = (app: any) => { @@ -127,7 +321,7 @@ export function AppManagement() { name: app.name, type: app.type, department: app.department, - creator: app.creator, + creator_id: app.creator_id || "", description: app.description, appUrl: app.appUrl, icon: app.icon || "Bot", @@ -141,25 +335,74 @@ export function AppManagement() { setShowDeleteConfirm(true) } - const confirmDeleteApp = () => { - if (selectedApp) { - setApps(apps.filter((app) => app.id !== selectedApp.id)) - setShowDeleteConfirm(false) - setSelectedApp(null) + const confirmDeleteApp = async () => { + try { + if (!selectedApp) return + + const response = await fetch(`/api/admin/apps/${selectedApp.id}`, { + method: 'DELETE' + }) + + const data = await response.json() + + if (data.success) { + await loadApps() + setShowDeleteConfirm(false) + setSelectedApp(null) + toast({ + title: "刪除成功", + description: `應用「${selectedApp.name}」已永久刪除`, + variant: "success", + }) + } else { + toast({ + title: "刪除失敗", + description: data.error || '刪除應用失敗', + variant: "destructive", + }) + } + } catch (error) { + console.error('刪除應用錯誤:', error) + toast({ + title: "錯誤", + description: '刪除應用時發生錯誤', + variant: "destructive", + }) } } - const handleToggleAppStatus = (appId: string) => { - setApps( - apps.map((app) => - app.id === appId - ? { - ...app, - status: app.status === "published" ? "draft" : "published", - } - : app, - ), - ) + const handleToggleAppStatus = async (appId: string) => { + try { + const response = await fetch(`/api/admin/apps/${appId}/toggle-status`, { + method: 'POST' + }) + + const data = await response.json() + + if (data.success) { + await loadApps() + // 根據新狀態顯示不同的成功訊息 + const newStatus = data.data?.app?.is_active ? '發布' : '下架' + toast({ + title: "狀態更新成功", + description: `應用已成功${newStatus}`, + variant: "success", + }) + } else { + toast({ + title: "狀態更新失敗", + description: data.error || '切換應用狀態失敗', + variant: "destructive", + }) + } + } catch (error) { + console.error('切換應用狀態錯誤:', error) + toast({ + title: "錯誤", + description: '切換應用狀態時發生錯誤', + variant: "destructive", + }) + } } const handleApprovalAction = (app: any, action: "approve" | "reject") => { @@ -187,45 +430,116 @@ export function AppManagement() { } } - const handleAddApp = () => { - const app = { - id: Date.now().toString(), - ...newApp, - status: "pending", - createdAt: new Date().toISOString().split("T")[0], - views: 0, - likes: 0, - rating: 0, - reviews: 0, + const handleAddApp = async () => { + try { + if (!newApp.creator_id) { + toast({ + title: "驗證失敗", + description: '請選擇創建者', + variant: "destructive", + }) + return + } + + const response = await fetch('/api/admin/apps', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + name: newApp.name, + description: newApp.description, + creator_id: newApp.creator_id, + category: newApp.type, // 使用 type 作為 category + type: newApp.type, + app_url: newApp.appUrl, + icon: newApp.icon, + icon_color: newApp.iconColor + }) + }) + + const data = await response.json() + + if (data.success) { + // 重新載入應用列表 + await loadApps() + setNewApp({ + name: "", + type: "文字處理", + department: "HQBU", + creator_id: "", + description: "", + appUrl: "", + icon: "Bot", + iconColor: "from-blue-500 to-purple-500", + }) + setShowAddApp(false) + } else { + toast({ + title: "創建失敗", + description: data.error || '創建應用失敗', + variant: "destructive", + }) + } + } catch (error) { + console.error('創建應用錯誤:', error) + toast({ + title: "錯誤", + description: '創建應用時發生錯誤', + variant: "destructive", + }) } - setApps([...apps, app]) - setNewApp({ - name: "", - type: "文字處理", - department: "HQBU", - creator: "", - description: "", - appUrl: "", - icon: "Bot", - iconColor: "from-blue-500 to-purple-500", - }) - setShowAddApp(false) } - const handleUpdateApp = () => { - if (selectedApp) { - setApps( - apps.map((app) => - app.id === selectedApp.id - ? { - ...app, - ...newApp, - } - : app, - ), - ) - setShowEditApp(false) - setSelectedApp(null) + const handleUpdateApp = async () => { + try { + if (!selectedApp) return + + if (!newApp.creator_id) { + toast({ + title: "驗證失敗", + description: '請選擇創建者', + variant: "destructive", + }) + return + } + + const response = await fetch(`/api/admin/apps/${selectedApp.id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + name: newApp.name, + description: newApp.description, + category: newApp.type, + type: newApp.type, + app_url: newApp.appUrl, + icon: newApp.icon, + icon_color: newApp.iconColor + }) + }) + + const data = await response.json() + + if (data.success) { + await loadApps() + setShowEditApp(false) + setSelectedApp(null) + } else { + toast({ + title: "更新失敗", + description: data.error || '更新應用失敗', + variant: "destructive", + }) + } + } catch (error) { + console.error('更新應用錯誤:', error) + toast({ + title: "錯誤", + description: '更新應用時發生錯誤', + variant: "destructive", + }) } } @@ -316,10 +630,10 @@ export function AppManagement() {
-

待審核

-

{apps.filter((a) => a.status === "pending").length}

+

已下架

+

{apps.filter((a) => a.status === "draft").length}

- +
@@ -344,11 +658,19 @@ export function AppManagement() { - 全部類型 - 文字處理 - 圖像生成 - 語音辨識 - 推薦系統 + 全部類型 + 文字處理 + 圖像生成 + 語音辨識 + 推薦系統 + 音樂生成 + 程式開發 + 影像處理 + 對話系統 + 數據分析 + 設計工具 + 語音技術 + 教育工具 @@ -451,8 +773,8 @@ export function AppManagement() {
- {app.rating} - ({app.reviews}) + {formatNumber(app.rating, 1)} + ({app.reviewCount || 0})
{app.createdAt} @@ -507,7 +829,7 @@ export function AppManagement() { )} handleDeleteApp(app)}> - 刪除應用 + 永久刪除 @@ -540,12 +862,25 @@ export function AppManagement() {
- setNewApp({ ...newApp, creator: e.target.value })} - placeholder="輸入創建者姓名" - /> +
@@ -557,10 +892,19 @@ export function AppManagement() { - 文字處理 - 圖像生成 - 語音辨識 - 推薦系統 + 全部類型 + 文字處理 + 圖像生成 + 語音辨識 + 推薦系統 + 音樂生成 + 程式開發 + 影像處理 + 對話系統 + 數據分析 + 設計工具 + 語音技術 + 教育工具 @@ -644,7 +988,7 @@ export function AppManagement() { - @@ -673,12 +1017,25 @@ export function AppManagement() {
- setNewApp({ ...newApp, creator: e.target.value })} - placeholder="輸入創建者姓名" - /> +
@@ -690,10 +1047,18 @@ export function AppManagement() { - 文字處理 - 圖像生成 - 語音辨識 - 推薦系統 + 文字處理 + 圖像生成 + 語音辨識 + 推薦系統 + 音樂生成 + 程式開發 + 影像處理 + 對話系統 + 數據分析 + 設計工具 + 語音技術 + 教育工具 @@ -775,7 +1140,7 @@ export function AppManagement() { - @@ -791,7 +1156,7 @@ export function AppManagement() { 確認刪除 - 您確定要刪除應用「{selectedApp?.name}」嗎?此操作無法復原。 + 您確定要永久刪除應用「{selectedApp?.name}」嗎?此操作無法復原,應用將從資料庫中完全移除。
-
+

{selectedApp.name}

{selectedApp.appUrl && ( +
+ + + ))} +
+ )} )} diff --git a/components/app-detail-dialog.tsx b/components/app-detail-dialog.tsx index b399644..d8610b0 100644 --- a/components/app-detail-dialog.tsx +++ b/components/app-detail-dialog.tsx @@ -57,6 +57,23 @@ export function AppDetailDialog({ open, onOpenChange, app }: AppDetailDialogProp const { user, addToRecentApps, getAppLikes, incrementViewCount, getViewCount, getAppRating } = useAuth() const [currentRating, setCurrentRating] = useState(getAppRating(app.id.toString())) const [reviewCount, setReviewCount] = useState(0) + const [appStats, setAppStats] = useState({ + basic: { + views: 0, + likes: 0, + rating: 0, + reviewCount: 0 + }, + usage: { + dailyUsers: 0, + weeklyUsers: 0, + monthlyUsers: 0, + totalSessions: 0, + topDepartments: [], + trendData: [] + } + }) + const [isLoadingStats, setIsLoadingStats] = useState(false) // Date range for usage trends const [startDate, setStartDate] = useState(() => { @@ -88,6 +105,34 @@ export function AppDetailDialog({ open, onOpenChange, app }: AppDetailDialogProp setReviewCount(newReviewCount) } + // 載入應用統計數據 + const loadAppStats = async () => { + if (!app.id) return + + setIsLoadingStats(true) + try { + const response = await fetch(`/api/apps/${app.id}/stats`) + const data = await response.json() + + if (data.success) { + setAppStats(data.data) + setCurrentRating(data.data.basic.rating) + setReviewCount(data.data.basic.reviewCount) + } + } catch (error) { + console.error('載入應用統計數據錯誤:', error) + } finally { + setIsLoadingStats(false) + } + } + + // 當對話框打開時載入統計數據 + React.useEffect(() => { + if (open && app.id) { + loadAppStats() + } + }, [open, app.id]) + const handleTryApp = () => { if (user) { addToRecentApps(app.id.toString()) @@ -245,19 +290,74 @@ export function AppDetailDialog({ open, onOpenChange, app }: AppDetailDialogProp -
+
+ -
- {/* Usage Overview */} + {/* 基本統計數據 */} +
+ + + 總瀏覽量 + + + +
+ {isLoadingStats ? '...' : appStats.basic.views} +
+

累計瀏覽次數

+
+
+ + + + 收藏數 + + + +
+ {isLoadingStats ? '...' : appStats.basic.likes} +
+

用戶收藏數量

+
+
+ + + + 平均評分 + + + +
+ {isLoadingStats ? '...' : appStats.basic.rating.toFixed(1)} +
+

用戶評分

+
+
+ + + + 評價數量 + + + +
+ {isLoadingStats ? '...' : appStats.basic.reviewCount} +
+

用戶評價總數

+
+
+
+ + {/* 使用趨勢 */}
@@ -265,7 +365,9 @@ export function AppDetailDialog({ open, onOpenChange, app }: AppDetailDialogProp -
{usageStats.dailyUsers}
+
+ {isLoadingStats ? '...' : appStats.usage.dailyUsers} +

活躍用戶數

@@ -276,7 +378,9 @@ export function AppDetailDialog({ open, onOpenChange, app }: AppDetailDialogProp -
{usageStats.weeklyUsers}
+
+ {isLoadingStats ? '...' : appStats.usage.weeklyUsers} +

週活躍用戶

@@ -287,7 +391,9 @@ export function AppDetailDialog({ open, onOpenChange, app }: AppDetailDialogProp -
{usageStats.totalSessions.toLocaleString()}
+
+ {isLoadingStats ? '...' : appStats.usage.totalSessions.toLocaleString()} +

累計使用次數

diff --git a/components/competition/popularity-rankings.tsx b/components/competition/popularity-rankings.tsx index 41eaab8..9d5b57c 100644 --- a/components/competition/popularity-rankings.tsx +++ b/components/competition/popularity-rankings.tsx @@ -704,11 +704,19 @@ export function PopularityRankings() { - 全部類型 - 文字處理 - 圖像生成 - 語音辨識 - 推薦系統 + 全部類型 + 文字處理 + 圖像生成 + 語音辨識 + 推薦系統 + 音樂生成 + 程式開發 + 影像處理 + 對話系統 + 數據分析 + 設計工具 + 語音技術 + 教育工具
diff --git a/components/competition/registration-dialog.tsx b/components/competition/registration-dialog.tsx index 467c930..b236c6b 100644 --- a/components/competition/registration-dialog.tsx +++ b/components/competition/registration-dialog.tsx @@ -202,12 +202,18 @@ export function RegistrationDialog({ open, onOpenChange }: RegistrationDialogPro - 文字處理 - 圖像生成 - 語音辨識 - 推薦系統 - 數據分析 - 自動化工具 + 文字處理 + 圖像生成 + 語音辨識 + 推薦系統 + 音樂生成 + 程式開發 + 影像處理 + 對話系統 + 數據分析 + 設計工具 + 語音技術 + 教育工具 其他 diff --git a/components/ui/toast.tsx b/components/ui/toast.tsx index ffe8207..2071c64 100644 --- a/components/ui/toast.tsx +++ b/components/ui/toast.tsx @@ -1,113 +1,183 @@ "use client" import * as React from "react" -import * as ToastPrimitives from "@radix-ui/react-toast" import { cva, type VariantProps } from "class-variance-authority" import { X } from "lucide-react" import { cn } from "@/lib/utils" -const ToastProvider = ToastPrimitives.Provider - -const ToastViewport = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -ToastViewport.displayName = ToastPrimitives.Viewport.displayName - const toastVariants = cva( - "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full", + "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all duration-300 ease-in-out transform", { variants: { variant: { default: "border bg-background text-foreground", - destructive: "destructive border-destructive bg-destructive text-destructive-foreground", + destructive: + "destructive group border-destructive bg-destructive text-destructive-foreground", + success: + "border-green-200 bg-green-50 text-green-900", + warning: + "border-yellow-200 bg-yellow-50 text-yellow-900", }, }, defaultVariants: { variant: "default", }, - }, + } ) const Toast = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef & VariantProps ->(({ className, variant, ...props }, ref) => { - return + HTMLDivElement, + React.HTMLAttributes & VariantProps & { + onOpenChange?: (open: boolean) => void + open?: boolean + } +>(({ className, variant, onOpenChange, open, ...props }, ref) => { + const [isVisible, setIsVisible] = React.useState(true) + const [isClosing, setIsClosing] = React.useState(false) + + // 處理關閉動畫 + const handleClose = React.useCallback(() => { + if (isClosing) return // 防止重複觸發 + + setIsClosing(true) + setIsVisible(false) + + // 等待動畫完成後調用 onOpenChange + setTimeout(() => { + onOpenChange?.(false) + }, 300) // 與 CSS 動畫時間一致 + }, [onOpenChange, isClosing]) + + // 處理外部觸發的關閉 + React.useEffect(() => { + if (open === false && !isClosing) { + handleClose() + } + }, [open, isClosing, handleClose]) + + return ( +
+ ) }) -Toast.displayName = ToastPrimitives.Root.displayName +Toast.displayName = "Toast" const ToastAction = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef + HTMLButtonElement, + React.ButtonHTMLAttributes >(({ className, ...props }, ref) => ( - )) -ToastAction.displayName = ToastPrimitives.Action.displayName +ToastAction.displayName = "ToastAction" const ToastClose = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - & { + onClose?: () => void + } +>(({ className, onClose, ...props }, ref) => ( + )) -ToastClose.displayName = ToastPrimitives.Close.displayName +ToastClose.displayName = "ToastClose" const ToastTitle = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef + HTMLDivElement, + React.HTMLAttributes >(({ className, ...props }, ref) => ( - +
)) -ToastTitle.displayName = ToastPrimitives.Title.displayName +ToastTitle.displayName = "ToastTitle" const ToastDescription = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef + HTMLDivElement, + React.HTMLAttributes >(({ className, ...props }, ref) => ( - +
)) -ToastDescription.displayName = ToastPrimitives.Description.displayName +ToastDescription.displayName = "ToastDescription" -type ToastProps = React.ComponentPropsWithoutRef +type ToastProps = React.ComponentProps type ToastActionElement = React.ReactElement +const ToastProvider = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +ToastProvider.displayName = "ToastProvider" + +const ToastViewport = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +ToastViewport.displayName = "ToastViewport" + export { type ToastProps, type ToastActionElement, - ToastProvider, - ToastViewport, Toast, + ToastAction, + ToastClose, ToastTitle, ToastDescription, - ToastClose, - ToastAction, -} + ToastProvider, + ToastViewport, +} \ No newline at end of file diff --git a/components/ui/toaster.tsx b/components/ui/toaster.tsx index bcf1cb3..74e68dc 100644 --- a/components/ui/toaster.tsx +++ b/components/ui/toaster.tsx @@ -1,24 +1,35 @@ "use client" -import { Toast, ToastClose, ToastDescription, ToastProvider, ToastTitle, ToastViewport } from "@/components/ui/toast" +import { + Toast, + ToastClose, + ToastDescription, + ToastProvider, + ToastTitle, + ToastViewport, +} from "@/components/ui/toast" import { useToast } from "@/hooks/use-toast" export function Toaster() { - const { toasts } = useToast() + const { toasts, dismiss } = useToast() return ( - {toasts.map(({ id, title, description, action, ...props }) => ( - -
- {title && {title}} - {description && {description}} -
- {action} - -
- ))} + {toasts.map(function ({ id, title, description, action, ...props }) { + return ( + dismiss(id)}> +
+ {title && {title}} + {description && ( + {description} + )} +
+ {action} + dismiss(id)} /> +
+ ) + })}
) -} +} \ No newline at end of file diff --git a/hooks/use-toast.ts b/hooks/use-toast.ts index 756585f..9d70fc2 100644 --- a/hooks/use-toast.ts +++ b/hooks/use-toast.ts @@ -2,7 +2,10 @@ import * as React from "react" -import type { ToastActionElement, ToastProps } from "@/components/ui/toast" +import type { + ToastActionElement, + ToastProps, +} from "@/components/ui/toast" const TOAST_LIMIT = 1 const TOAST_REMOVE_DELAY = 1000000 @@ -12,6 +15,7 @@ type ToasterToast = ToastProps & { title?: React.ReactNode description?: React.ReactNode action?: ToastActionElement + open?: boolean } const actionTypes = { @@ -81,14 +85,14 @@ export const reducer = (state: State, action: Action): State => { case "UPDATE_TOAST": return { ...state, - toasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t)), + toasts: state.toasts.map((t) => + t.id === action.toast.id ? { ...t, ...action.toast } : t + ), } case "DISMISS_TOAST": { const { toastId } = action - // ! Side effects ! - This could be extracted into a dismissToast() action, - // but I'll keep it here for simplicity if (toastId) { addToRemoveQueue(toastId) } else { @@ -105,7 +109,7 @@ export const reducer = (state: State, action: Action): State => { ...t, open: false, } - : t, + : t ), } } @@ -181,8 +185,8 @@ function useToast() { return { ...state, toast, - dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), + dismiss: (toastId?: string) => dispatch({ type: "REMOVE_TOAST", toastId }), } } -export { useToast, toast } +export { useToast, toast } \ No newline at end of file diff --git a/lib/services/database-service.ts b/lib/services/database-service.ts index afb9743..a148686 100644 --- a/lib/services/database-service.ts +++ b/lib/services/database-service.ts @@ -917,61 +917,552 @@ export class CompetitionService { // ===================================================== export class AppService { // 創建應用 - static async createApp(appData: Omit): Promise { + async createApp(appData: { + name: string; + description: string; + creator_id: string; + category: string; + type: string; + app_url?: string; + icon?: string; + icon_color?: string; + }): Promise<{ success: boolean; app?: any; error?: string }> { + try { + const appId = crypto.randomUUID(); + const sql = ` + INSERT INTO apps (id, name, description, creator_id, category, type, app_url, icon, icon_color, likes_count, views_count, rating, is_active, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0, 0.00, TRUE, NOW(), NOW()) + `; + + await this.query(sql, [ + appId, + appData.name, + appData.description, + appData.creator_id, + appData.category, + appData.type, + appData.app_url || null, + appData.icon || 'Bot', + appData.icon_color || 'from-blue-500 to-purple-500' + ]); + + // 獲取創建的應用 + const createdApp = await this.getAppById(appId); + + return { success: true, app: createdApp }; + } catch (error) { + console.error('創建應用錯誤:', error); + return { success: false, error: '創建應用時發生錯誤' }; + } + } + + // 根據 ID 獲取應用(僅已發布) + async getAppById(appId: string): Promise { const sql = ` - INSERT INTO apps (id, name, description, creator_id, team_id, category, type, likes_count, views_count, rating, is_active) - VALUES (UUID(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + SELECT + a.*, + u.name as creator_name, + u.department as creator_department + FROM apps a + LEFT JOIN users u ON a.creator_id = u.id + WHERE a.id = ? AND a.is_active = TRUE `; - const params = [ - appData.name, - appData.description || null, - appData.creator_id, - appData.team_id || null, - appData.category, - appData.type, - appData.likes_count, - appData.views_count, - appData.rating, - appData.is_active - ]; - - await db.insert(sql, params); - return await this.getAppByName(appData.name) as App; + return await this.queryOne(sql, [appId]); + } + + // 根據 ID 獲取應用(任何狀態) + async getAppByIdAnyStatus(appId: string): Promise { + const sql = ` + SELECT + a.*, + u.name as creator_name, + u.department as creator_department + FROM apps a + LEFT JOIN users u ON a.creator_id = u.id + WHERE a.id = ? + `; + return await this.queryOne(sql, [appId]); } // 根據名稱獲取應用 - static async getAppByName(name: string): Promise { - const sql = 'SELECT * FROM apps WHERE name = ? AND is_active = TRUE'; - return await db.queryOne(sql, [name]); - } - - // 根據ID獲取應用 - static async getAppById(id: string): Promise { - const sql = 'SELECT * FROM apps WHERE id = ? AND is_active = TRUE'; - return await db.queryOne(sql, [id]); + async getAppByName(name: string): Promise { + const sql = ` + SELECT + a.*, + u.name as creator_name, + u.department as creator_department + FROM apps a + LEFT JOIN users u ON a.creator_id = u.id + WHERE a.name = ? AND a.is_active = TRUE + `; + return await this.queryOne(sql, [name]); } // 獲取所有應用 - static async getAllApps(limit = 50, offset = 0): Promise { - const sql = 'SELECT * FROM apps WHERE is_active = TRUE ORDER BY created_at DESC LIMIT ? OFFSET ?'; - return await db.query(sql, [limit, offset]); - } + async getAllApps(filters: { + search?: string; + category?: string; + type?: string; + status?: string; + page?: number; + limit?: number; + } = {}): Promise<{ apps: any[]; total: number }> { + try { + const { search = '', category = 'all', type = 'all', status = 'all', page = 1, limit = 10 } = filters; + + // 構建查詢條件 + let whereConditions: string[] = []; + let params: any[] = []; - // 獲取應用統計 - static async getAppStatistics(id: string): Promise { - const sql = 'SELECT * FROM app_statistics WHERE id = ?'; - return await db.queryOne(sql, [id]); + // 根據狀態篩選 + if (status && status !== 'all') { + if (status === 'active') { + whereConditions.push('a.is_active = TRUE'); + } else if (status === 'inactive') { + whereConditions.push('a.is_active = FALSE'); + } + // 如果 status 是 'all' 或其他值,則不添加狀態篩選 + } + + if (search) { + whereConditions.push('(a.name LIKE ? OR a.description LIKE ? OR u.name LIKE ?)'); + params.push(`%${search}%`, `%${search}%`, `%${search}%`); + } + + if (category && category !== 'all') { + whereConditions.push('a.category = ?'); + params.push(category); + } + + if (type && type !== 'all') { + whereConditions.push('a.type = ?'); + params.push(type); + } + + const whereClause = whereConditions.length > 0 ? `WHERE ${whereConditions.join(' AND ')}` : ''; + + // 獲取總數 + const countSql = ` + SELECT COUNT(DISTINCT a.id) as total + FROM apps a + LEFT JOIN users u ON a.creator_id = u.id + LEFT JOIN user_ratings ur ON a.id = ur.app_id + ${whereClause} + `; + const countResult = await this.queryOne(countSql, params); + const total = countResult?.total || 0; + + // 獲取應用列表 + const offset = (page - 1) * limit; + const sql = ` + SELECT + a.*, + u.name as creator_name, + u.department as creator_department, + u.email as creator_email, + COALESCE(AVG(ur.rating), 0) as rating, + COUNT(ur.id) as reviewCount + FROM apps a + LEFT JOIN users u ON a.creator_id = u.id + LEFT JOIN user_ratings ur ON a.id = ur.app_id + ${whereClause} + GROUP BY a.id, u.name, u.department, u.email + ORDER BY a.created_at DESC + LIMIT ${limit} OFFSET ${offset} + `; + + const apps = await this.query(sql, params); + + return { apps, total }; + } catch (error) { + console.error('獲取應用列表錯誤:', error); + return { apps: [], total: 0 }; + } } // 更新應用 - static async updateApp(id: string, updates: Partial): Promise { - const fields = Object.keys(updates).filter(key => key !== 'id' && key !== 'created_at'); - const setClause = fields.map(field => `${field} = ?`).join(', '); - const values = fields.map(field => (updates as any)[field]); - - const sql = `UPDATE apps SET ${setClause}, updated_at = CURRENT_TIMESTAMP WHERE id = ?`; - const result = await db.update(sql, [...values, id]); - return result.affectedRows > 0; + async updateApp(appId: string, updates: { + name?: string; + description?: string; + category?: string; + type?: string; + app_url?: string; + icon?: string; + icon_color?: string; + }): Promise<{ success: boolean; app?: any; error?: string }> { + try { + const updateFields = []; + const params = []; + + if (updates.name !== undefined) { + updateFields.push('name = ?'); + params.push(updates.name); + } + if (updates.description !== undefined) { + updateFields.push('description = ?'); + params.push(updates.description); + } + if (updates.category !== undefined) { + updateFields.push('category = ?'); + params.push(updates.category); + } + if (updates.type !== undefined) { + updateFields.push('type = ?'); + params.push(updates.type); + } + if (updates.app_url !== undefined) { + updateFields.push('app_url = ?'); + params.push(updates.app_url); + } + if (updates.icon !== undefined) { + updateFields.push('icon = ?'); + params.push(updates.icon); + } + if (updates.icon_color !== undefined) { + updateFields.push('icon_color = ?'); + params.push(updates.icon_color); + } + + if (updateFields.length === 0) { + return { success: false, error: '沒有要更新的欄位' }; + } + + updateFields.push('updated_at = NOW()'); + params.push(appId); + + const sql = ` + UPDATE apps + SET ${updateFields.join(', ')} + WHERE id = ? AND is_active = TRUE + `; + + await this.query(sql, params); + + // 獲取更新後的應用 + const updatedApp = await this.getAppById(appId); + + return { success: true, app: updatedApp }; + } catch (error) { + console.error('更新應用錯誤:', error); + return { success: false, error: '更新應用時發生錯誤' }; + } + } + + // 刪除應用(硬刪除) + async deleteApp(appId: string): Promise<{ success: boolean; error?: string }> { + try { + // 先檢查應用是否存在(任何狀態) + const app = await this.getAppByIdAnyStatus(appId); + if (!app) { + return { success: false, error: '應用不存在' }; + } + + // 硬刪除應用(從資料庫中完全移除) + const sql = 'DELETE FROM apps WHERE id = ?'; + await this.query(sql, [appId]); + return { success: true }; + } catch (error) { + console.error('刪除應用錯誤:', error); + return { success: false, error: '刪除應用時發生錯誤' }; + } + } + + // 切換應用狀態 + async toggleAppStatus(appId: string): Promise<{ success: boolean; app?: any; error?: string }> { + try { + // 先獲取當前狀態(任何狀態) + const currentApp = await this.getAppByIdAnyStatus(appId); + if (!currentApp) { + return { success: false, error: '應用不存在' }; + } + + const newStatus = currentApp.is_active ? false : true; + const sql = 'UPDATE apps SET is_active = ?, updated_at = NOW() WHERE id = ?'; + await this.query(sql, [newStatus, appId]); + + // 獲取更新後的應用(任何狀態) + const updatedApp = await this.getAppByIdAnyStatus(appId); + + return { success: true, app: updatedApp }; + } catch (error) { + console.error('切換應用狀態錯誤:', error); + return { success: false, error: '切換應用狀態時發生錯誤' }; + } + } + + // 獲取應用統計 + async getAppStats(): Promise<{ + totalApps: number; + activeApps: number; + inactiveApps: number; + pendingApps: number; + totalViews: number; + totalLikes: number; + newThisMonth: number; + }> { + try { + const sql = ` + SELECT + COUNT(*) as total_apps, + COUNT(CASE WHEN is_active = TRUE THEN 1 END) as active_apps, + COUNT(CASE WHEN is_active = FALSE THEN 1 END) as inactive_apps, + COALESCE(SUM(views_count), 0) as total_views, + COALESCE(SUM(likes_count), 0) as total_likes, + COUNT(CASE WHEN created_at >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) THEN 1 END) as new_this_month + FROM apps + `; + + const result = await this.queryOne(sql); + + return { + totalApps: result?.total_apps || 0, + activeApps: result?.active_apps || 0, + inactiveApps: result?.inactive_apps || 0, + pendingApps: 0, // 目前沒有 pending 狀態 + totalViews: result?.total_views || 0, + totalLikes: result?.total_likes || 0, + newThisMonth: result?.new_this_month || 0 + }; + } catch (error) { + console.error('獲取應用統計錯誤:', error); + return { + totalApps: 0, + activeApps: 0, + inactiveApps: 0, + pendingApps: 0, + totalViews: 0, + totalLikes: 0, + newThisMonth: 0 + }; + } + } + + // 通用查詢方法 + async query(sql: string, params: any[] = []): Promise { + return await db.query(sql, params); + } + + // 通用單一查詢方法 + async queryOne(sql: string, params: any[] = []): Promise { + return await db.queryOne(sql, params); + } + + // 獲取應用評分統計 + async getAppRatingStats(appId: string): Promise<{ + averageRating: number; + totalRatings: number; + ratingDistribution: { rating: number; count: number }[]; + }> { + try { + const sql = ` + SELECT + AVG(rating) as average_rating, + COUNT(*) as total_ratings + FROM user_ratings + WHERE app_id = ? + `; + + const result = await this.queryOne(sql, [appId]); + + const distributionSql = ` + SELECT + rating, + COUNT(*) as count + FROM user_ratings + WHERE app_id = ? + GROUP BY rating + ORDER BY rating DESC + `; + + const distribution = await this.query(distributionSql, [appId]); + + return { + averageRating: result.average_rating ? parseFloat(result.average_rating) : 0, + totalRatings: result.total_ratings || 0, + ratingDistribution: distribution.map((row: any) => ({ + rating: row.rating, + count: row.count + })) + }; + } catch (error) { + console.error('獲取應用評分統計錯誤:', error); + return { + averageRating: 0, + totalRatings: 0, + ratingDistribution: [] + }; + } + } + + // 獲取應用使用統計 + async getAppUsageStats(appId: string): Promise<{ + dailyUsers: number; + weeklyUsers: number; + monthlyUsers: number; + totalSessions: number; + topDepartments: { department: string; count: number }[]; + trendData: { date: string; users: number }[]; + }> { + try { + // 今日使用者 + const dailySql = ` + SELECT COUNT(DISTINCT user_id) as daily_users + FROM user_views + WHERE app_id = ? AND DATE(viewed_at) = CURDATE() + `; + const dailyResult = await this.queryOne(dailySql, [appId]); + + // 本週使用者 + const weeklySql = ` + SELECT COUNT(DISTINCT user_id) as weekly_users + FROM user_views + WHERE app_id = ? AND viewed_at >= DATE_SUB(CURDATE(), INTERVAL 1 WEEK) + `; + const weeklyResult = await this.queryOne(weeklySql, [appId]); + + // 本月使用者 + const monthlySql = ` + SELECT COUNT(DISTINCT user_id) as monthly_users + FROM user_views + WHERE app_id = ? AND viewed_at >= DATE_SUB(CURDATE(), INTERVAL 1 MONTH) + `; + const monthlyResult = await this.queryOne(monthlySql, [appId]); + + // 總使用次數 + const totalSql = ` + SELECT COUNT(*) as total_sessions + FROM user_views + WHERE app_id = ? + `; + const totalResult = await this.queryOne(totalSql, [appId]); + + // 部門使用統計 + const deptSql = ` + SELECT + u.department, + COUNT(*) as count + FROM user_views uv + JOIN users u ON uv.user_id = u.id + WHERE uv.app_id = ? + GROUP BY u.department + ORDER BY count DESC + LIMIT 5 + `; + const deptResult = await this.query(deptSql, [appId]); + + // 使用趨勢(過去7天) + const trendSql = ` + SELECT + DATE(viewed_at) as date, + COUNT(DISTINCT user_id) as users + FROM user_views + WHERE app_id = ? AND viewed_at >= DATE_SUB(CURDATE(), INTERVAL 7 DAY) + GROUP BY DATE(viewed_at) + ORDER BY date ASC + `; + const trendResult = await this.query(trendSql, [appId]); + + return { + dailyUsers: dailyResult.daily_users || 0, + weeklyUsers: weeklyResult.weekly_users || 0, + monthlyUsers: monthlyResult.monthly_users || 0, + totalSessions: totalResult.total_sessions || 0, + topDepartments: deptResult.map((row: any) => ({ + department: row.department, + count: row.count + })), + trendData: trendResult.map((row: any) => ({ + date: row.date, + users: row.users + })) + }; + } catch (error) { + console.error('獲取應用使用統計錯誤:', error); + return { + dailyUsers: 0, + weeklyUsers: 0, + monthlyUsers: 0, + totalSessions: 0, + topDepartments: [], + trendData: [] + }; + } + } + + // 獲取應用評價列表 + async getAppReviews(appId: string, limit: number = 10, offset: number = 0): Promise { + try { + const sql = ` + SELECT + ur.id, + ur.rating, + ur.rated_at, + u.name as user_name, + u.department as user_department, + u.avatar as user_avatar + FROM user_ratings ur + JOIN users u ON ur.user_id = u.id + WHERE ur.app_id = ? + ORDER BY ur.rated_at DESC + LIMIT ${limit} OFFSET ${offset} + `; + + const reviews = await this.query(sql, [appId]); + return reviews.map((review: any) => ({ + id: review.id, + rating: review.rating, + review: '用戶評價', // 暫時使用固定文字,因為資料庫中沒有 review 欄位 + ratedAt: review.rated_at, + userName: review.user_name, + userDepartment: review.user_department, + userAvatar: review.user_avatar + })); + } catch (error) { + console.error('獲取應用評價列表錯誤:', error); + return []; + } + } + + // 獲取應用評價總數 + async getAppReviewCount(appId: string): Promise { + try { + const sql = ` + SELECT COUNT(*) as count + FROM user_ratings + WHERE app_id = ? + `; + + const result = await this.queryOne(sql, [appId]); + return result.count || 0; + } catch (error) { + console.error('獲取應用評價總數錯誤:', error); + return 0; + } + } + + // 刪除評價 + async deleteReview(reviewId: string, appId: string): Promise<{ success: boolean; error?: string }> { + try { + // 檢查評價是否存在且屬於該應用 + const checkSql = ` + SELECT id FROM user_ratings + WHERE id = ? AND app_id = ? + `; + const review = await this.queryOne(checkSql, [reviewId, appId]); + + if (!review) { + return { success: false, error: '評價不存在或無權限刪除' }; + } + + // 刪除評價 + const deleteSql = 'DELETE FROM user_ratings WHERE id = ?'; + await this.query(deleteSql, [reviewId]); + + return { success: true }; + } catch (error) { + console.error('刪除評價錯誤:', error); + return { success: false, error: '刪除評價時發生錯誤' }; + } } } diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..394084b --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,80 @@ +# 測試數據插入說明 + +## 問題修復 + +✅ **已修復重複約束錯誤**:原來的腳本會因為同一個用戶對同一個應用重複評分而出現 `Duplicate entry` 錯誤。現在已修復為使用不同的用戶 ID。 + +✅ **已修復 null 值錯誤**:修復了 `user_id` 不能為 null 的問題,提供了簡化版本的腳本。 + +## 腳本版本 + +### 版本 1:完整版本 (`insert-test-data.sql`) +- 使用不同的用戶 ID 避免重複約束 +- 需要資料庫中有至少 10 個活躍用戶 + +### 版本 2:簡化版本 (`insert-test-data-simple.sql`) +- 使用單一用戶 ID,避免 null 值問題 +- 只需要資料庫中有至少 1 個活躍用戶 +- 只有 1 條評分記錄 + +### 版本 3:多用戶版本 (`insert-test-data-multi-user.sql`) ⭐ 推薦 +- 使用 5 個不同用戶,避免重複約束 +- 需要資料庫中有至少 5 個活躍用戶 +- 包含 25 條瀏覽、8 條按讚、5 條評分記錄 +- 平衡了數據豐富度和穩定性 + +## 方法一:使用 MySQL 命令行 + +1. 打開命令提示符或 PowerShell +2. 連接到 MySQL: + ```bash + mysql -u root -p + ``` +3. 選擇資料庫: + ```sql + USE ai_showcase_platform; + ``` +4. 執行 SQL 腳本(推薦使用多用戶版本): + ```sql + source E:/ai-showcase-platform/scripts/insert-test-data-multi-user.sql + ``` + +## 方法二:使用 MySQL Workbench 或其他 GUI 工具 + +1. 打開 MySQL Workbench +2. 連接到您的 MySQL 服務器 +3. 選擇 `ai_showcase_platform` 資料庫 +4. 打開 `scripts/insert-test-data-multi-user.sql` 文件(推薦) +5. 執行整個腳本 + +## 方法三:使用 phpMyAdmin + +1. 打開 phpMyAdmin +2. 選擇 `ai_showcase_platform` 資料庫 +3. 點擊 "SQL" 標籤 +4. 複製 `scripts/insert-test-data-multi-user.sql` 的內容(推薦) +5. 貼上並執行 + +## 預期結果 + +執行成功後,您應該看到: +- 25 條瀏覽記錄(使用 5 個不同用戶) +- 8 條按讚記錄(使用 5 個不同用戶) +- 5 條評分記錄(使用 5 個不同用戶) +- 應用統計數據更新為:25 瀏覽、8 讚、4.2 平均評分 + +## 驗證 + +執行完成後,您可以: +1. 重新載入應用管理頁面 +2. 點擊任何應用的「查看詳情」 +3. 切換到「統計數據」標籤頁查看真實數據 +4. 切換到「評價管理」標籤頁查看評價列表 + +## 注意事項 + +- 腳本會先清空現有的測試數據,避免重複 +- **簡化版本**:使用單一用戶 ID,只有 1 條評分記錄 +- **多用戶版本**:使用 5 個不同用戶,平衡數據豐富度和穩定性 ⭐ 推薦 +- **完整版本**:使用不同用戶 ID,需要至少 10 個活躍用戶 +- 如果您的資料庫中用戶數量少於 5 個,建議使用簡化版本 diff --git a/scripts/insert-real-test-data.js b/scripts/insert-real-test-data.js new file mode 100644 index 0000000..40ccb7e --- /dev/null +++ b/scripts/insert-real-test-data.js @@ -0,0 +1,180 @@ +const mysql = require('mysql2/promise'); + +async function insertTestData() { + console.log('📊 在資料庫中插入真實測試數據...\n'); + + let connection; + try { + // 連接資料庫 + connection = await mysql.createConnection({ + host: 'localhost', + user: 'root', + password: '123456', + database: 'ai_showcase_platform' + }); + + console.log('✅ 資料庫連接成功'); + + // 1. 獲取現有的應用和用戶 + const [apps] = await connection.execute('SELECT id FROM apps WHERE is_active = TRUE LIMIT 3'); + const [users] = await connection.execute('SELECT id FROM users WHERE status = "active" LIMIT 10'); + + if (apps.length === 0) { + console.log('❌ 沒有找到現有應用,無法創建測試數據'); + return; + } + + console.log(`📱 找到 ${apps.length} 個應用`); + console.log(`👥 找到 ${users.length} 個用戶`); + + const targetAppId = apps[0].id; // 使用第一個應用作為目標 + console.log(`🎯 目標應用 ID: ${targetAppId}`); + + // 2. 清空現有的測試數據(如果有的話) + console.log('\n🧹 清空現有測試數據...'); + await connection.execute('DELETE FROM user_views WHERE app_id = ?', [targetAppId]); + await connection.execute('DELETE FROM user_likes WHERE app_id = ?', [targetAppId]); + await connection.execute('DELETE FROM user_ratings WHERE app_id = ?', [targetAppId]); + console.log('✅ 現有測試數據已清空'); + + // 3. 插入用戶瀏覽記錄 + console.log('\n👀 插入用戶瀏覽記錄...'); + const views = []; + for (let i = 0; i < 25; i++) { + const userId = users[Math.floor(Math.random() * users.length)].id; + const viewedAt = new Date(Date.now() - Math.random() * 30 * 24 * 60 * 60 * 1000); // 過去30天內 + const ipAddress = `192.168.1.${Math.floor(Math.random() * 255)}`; + const userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'; + + views.push([targetAppId, userId, viewedAt, ipAddress, userAgent]); + } + + for (const view of views) { + await connection.execute(` + INSERT INTO user_views (id, app_id, user_id, viewed_at, ip_address, user_agent) + VALUES (UUID(), ?, ?, ?, ?, ?) + `, view); + } + + console.log(`✅ 插入了 ${views.length} 個瀏覽記錄`); + + // 4. 插入用戶按讚記錄 + console.log('\n❤️ 插入用戶按讚記錄...'); + const likes = []; + for (let i = 0; i < 8; i++) { + const userId = users[Math.floor(Math.random() * users.length)].id; + const likedAt = new Date(Date.now() - Math.random() * 30 * 24 * 60 * 60 * 1000); + + likes.push([targetAppId, userId, likedAt]); + } + + for (const like of likes) { + await connection.execute(` + INSERT INTO user_likes (id, app_id, user_id, liked_at) + VALUES (UUID(), ?, ?, ?) + `, like); + } + + console.log(`✅ 插入了 ${likes.length} 個按讚記錄`); + + // 5. 插入用戶評分記錄 + console.log('\n⭐ 插入用戶評分記錄...'); + const ratings = [ + { rating: 5, review: '這個應用非常實用,界面設計得很棒!' }, + { rating: 4, review: '功能強大,處理速度很快,推薦使用。' }, + { rating: 5, review: '非常好用的工具,節省了很多時間。' }, + { rating: 3, review: '還不錯,但還有改進空間。' }, + { rating: 4, review: '界面簡潔,操作簡單,很滿意。' }, + { rating: 5, review: '功能齊全,使用體驗很好。' }, + { rating: 4, review: '整體不錯,希望能增加更多功能。' }, + { rating: 5, review: '非常推薦,已經推薦給同事了。' }, + { rating: 3, review: '還可以,但界面可以再優化。' }, + { rating: 4, review: '實用性很高,值得使用。' } + ]; + + for (let i = 0; i < ratings.length; i++) { + const userId = users[Math.floor(Math.random() * users.length)].id; + const ratedAt = new Date(Date.now() - Math.random() * 30 * 24 * 60 * 60 * 1000); + + await connection.execute(` + INSERT INTO user_ratings (id, app_id, user_id, rating, rated_at) + VALUES (UUID(), ?, ?, ?, ?) + `, [targetAppId, userId, ratings[i].rating, ratedAt]); + } + + console.log(`✅ 插入了 ${ratings.length} 個評分記錄`); + + // 6. 更新應用的統計數據 + console.log('\n📈 更新應用統計數據...'); + const [viewCount] = await connection.execute( + 'SELECT COUNT(*) as count FROM user_views WHERE app_id = ?', + [targetAppId] + ); + const [likeCount] = await connection.execute( + 'SELECT COUNT(*) as count FROM user_likes WHERE app_id = ?', + [targetAppId] + ); + + await connection.execute(` + UPDATE apps + SET views_count = ?, likes_count = ?, updated_at = NOW() + WHERE id = ? + `, [viewCount[0].count, likeCount[0].count, targetAppId]); + + console.log(` - 應用 ${targetAppId}: ${viewCount[0].count} 瀏覽, ${likeCount[0].count} 讚`); + + // 7. 顯示最終統計 + console.log('\n📊 數據插入完成!最終統計:'); + + const [appStats] = await connection.execute(` + SELECT + views_count, + likes_count + FROM apps + WHERE id = ? + `, [targetAppId]); + + const [ratingStats] = await connection.execute(` + SELECT + COUNT(*) as total_ratings, + AVG(rating) as avg_rating + FROM user_ratings + WHERE app_id = ? + `, [targetAppId]); + + const [viewStats] = await connection.execute(` + SELECT COUNT(*) as total_views FROM user_views WHERE app_id = ? + `, [targetAppId]); + + const [likeStats] = await connection.execute(` + SELECT COUNT(*) as total_likes FROM user_likes WHERE app_id = ? + `, [targetAppId]); + + console.log(` - 應用瀏覽數: ${appStats[0].views_count}`); + console.log(` - 應用讚數: ${appStats[0].likes_count}`); + console.log(` - 總瀏覽數: ${viewStats[0].total_views}`); + console.log(` - 總讚數: ${likeStats[0].total_likes}`); + console.log(` - 評分總數: ${ratingStats[0].total_ratings}`); + console.log(` - 平均評分: ${ratingStats[0].avg_rating.toFixed(1)}`); + + console.log('\n🎉 真實測試數據插入完成!'); + console.log('\n💡 現在您可以重新載入應用管理頁面查看真實的資料庫數據'); + + } catch (error) { + console.error('❌ 插入測試數據時發生錯誤:', error); + if (error.code === 'ECONNREFUSED') { + console.log('\n💡 請確保 MySQL 服務正在運行,並且連接參數正確'); + console.log(' - Host: localhost'); + console.log(' - User: root'); + console.log(' - Password: 123456'); + console.log(' - Database: ai_showcase_platform'); + } + } finally { + if (connection) { + await connection.end(); + console.log('\n🔌 資料庫連接已關閉'); + } + } +} + +insertTestData(); diff --git a/scripts/insert-test-data-multi-user.sql b/scripts/insert-test-data-multi-user.sql new file mode 100644 index 0000000..111b58c --- /dev/null +++ b/scripts/insert-test-data-multi-user.sql @@ -0,0 +1,80 @@ +-- 插入真實測試數據到資料庫(多用戶版本) +-- 請在 MySQL 中執行此腳本 + +-- 1. 檢查是否有足夠的用戶 +SELECT COUNT(*) as user_count FROM users WHERE status = 'active'; + +-- 2. 清空現有的測試數據(如果有的話) +DELETE FROM user_views WHERE app_id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7'; +DELETE FROM user_likes WHERE app_id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7'; +DELETE FROM user_ratings WHERE app_id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7'; + +-- 3. 獲取多個用戶的 ID +SET @user1_id = (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 0); +SET @user2_id = (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 1); +SET @user3_id = (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 2); +SET @user4_id = (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 3); +SET @user5_id = (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 4); + +-- 4. 插入用戶瀏覽記錄 (25條) - 使用多個用戶 +INSERT INTO user_views (id, app_id, user_id, viewed_at, ip_address, user_agent) VALUES +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user1_id, '2024-01-20 10:30:00', '192.168.1.100', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user2_id, '2024-01-19 14:20:00', '192.168.1.101', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user3_id, '2024-01-18 09:15:00', '192.168.1.102', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user4_id, '2024-01-17 16:45:00', '192.168.1.103', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user5_id, '2024-01-16 11:30:00', '192.168.1.104', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user1_id, '2024-01-15 08:20:00', '192.168.1.105', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user2_id, '2024-01-14 15:10:00', '192.168.1.106', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user3_id, '2024-01-13 12:45:00', '192.168.1.107', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user4_id, '2024-01-12 17:30:00', '192.168.1.108', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user5_id, '2024-01-11 09:15:00', '192.168.1.109', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user1_id, '2024-01-10 14:25:00', '192.168.1.110', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user2_id, '2024-01-09 11:40:00', '192.168.1.111', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user3_id, '2024-01-08 16:55:00', '192.168.1.112', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user4_id, '2024-01-07 13:20:00', '192.168.1.113', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user5_id, '2024-01-06 10:35:00', '192.168.1.114', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user1_id, '2024-01-05 15:50:00', '192.168.1.115', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user2_id, '2024-01-04 12:15:00', '192.168.1.116', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user3_id, '2024-01-03 09:30:00', '192.168.1.117', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user4_id, '2024-01-02 17:45:00', '192.168.1.118', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user5_id, '2024-01-01 14:20:00', '192.168.1.119', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user1_id, '2023-12-31 11:10:00', '192.168.1.120', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user2_id, '2023-12-30 16:25:00', '192.168.1.121', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user3_id, '2023-12-29 13:40:00', '192.168.1.122', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user4_id, '2023-12-28 10:55:00', '192.168.1.123', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user5_id, '2023-12-27 15:15:00', '192.168.1.124', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'); + +-- 5. 插入用戶按讚記錄 (8條) - 使用多個用戶 +INSERT INTO user_likes (id, app_id, user_id, liked_at) VALUES +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user1_id, '2024-01-20 10:35:00'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user2_id, '2024-01-19 14:25:00'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user3_id, '2024-01-18 09:20:00'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user4_id, '2024-01-17 16:50:00'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user5_id, '2024-01-16 11:35:00'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user1_id, '2024-01-15 08:25:00'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user2_id, '2024-01-14 15:15:00'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user3_id, '2024-01-13 12:50:00'); + +-- 6. 插入用戶評分記錄 (5條) - 使用不同用戶,避免重複約束 +INSERT INTO user_ratings (id, app_id, user_id, rating, rated_at) VALUES +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user1_id, 5, '2024-01-20 10:40:00'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user2_id, 4, '2024-01-19 14:30:00'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user3_id, 5, '2024-01-18 09:25:00'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user4_id, 3, '2024-01-17 16:55:00'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user5_id, 4, '2024-01-16 11:40:00'); + +-- 7. 更新應用的統計數據 +UPDATE apps +SET + views_count = (SELECT COUNT(*) FROM user_views WHERE app_id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7'), + likes_count = (SELECT COUNT(*) FROM user_likes WHERE app_id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7'), + updated_at = NOW() +WHERE id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7'; + +-- 8. 顯示統計摘要 +SELECT + '統計摘要' as 項目, + (SELECT COUNT(*) FROM user_views WHERE app_id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7') as 總瀏覽數, + (SELECT COUNT(*) FROM user_likes WHERE app_id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7') as 總讚數, + (SELECT COUNT(*) FROM user_ratings WHERE app_id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7') as 總評分數, + (SELECT AVG(rating) FROM user_ratings WHERE app_id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7') as 平均評分; diff --git a/scripts/insert-test-data-simple.sql b/scripts/insert-test-data-simple.sql new file mode 100644 index 0000000..b15e73d --- /dev/null +++ b/scripts/insert-test-data-simple.sql @@ -0,0 +1,72 @@ +-- 插入真實測試數據到資料庫(簡化版本) +-- 請在 MySQL 中執行此腳本 + +-- 1. 檢查是否有足夠的用戶 +SELECT COUNT(*) as user_count FROM users WHERE status = 'active'; + +-- 2. 清空現有的測試數據(如果有的話) +DELETE FROM user_views WHERE app_id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7'; +DELETE FROM user_likes WHERE app_id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7'; +DELETE FROM user_ratings WHERE app_id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7'; + +-- 3. 獲取第一個用戶的 ID 作為默認用戶 +SET @default_user_id = (SELECT id FROM users WHERE status = 'active' LIMIT 1); + +-- 4. 插入用戶瀏覽記錄 (25條) - 使用默認用戶 +INSERT INTO user_views (id, app_id, user_id, viewed_at, ip_address, user_agent) VALUES +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2024-01-20 10:30:00', '192.168.1.100', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2024-01-19 14:20:00', '192.168.1.101', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2024-01-18 09:15:00', '192.168.1.102', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2024-01-17 16:45:00', '192.168.1.103', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2024-01-16 11:30:00', '192.168.1.104', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2024-01-15 08:20:00', '192.168.1.105', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2024-01-14 15:10:00', '192.168.1.106', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2024-01-13 12:45:00', '192.168.1.107', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2024-01-12 17:30:00', '192.168.1.108', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2024-01-11 09:15:00', '192.168.1.109', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2024-01-10 14:25:00', '192.168.1.110', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2024-01-09 11:40:00', '192.168.1.111', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2024-01-08 16:55:00', '192.168.1.112', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2024-01-07 13:20:00', '192.168.1.113', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2024-01-06 10:35:00', '192.168.1.114', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2024-01-05 15:50:00', '192.168.1.115', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2024-01-04 12:15:00', '192.168.1.116', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2024-01-03 09:30:00', '192.168.1.117', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2024-01-02 17:45:00', '192.168.1.118', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2024-01-01 14:20:00', '192.168.1.119', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2023-12-31 11:10:00', '192.168.1.120', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2023-12-30 16:25:00', '192.168.1.121', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2023-12-29 13:40:00', '192.168.1.122', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2023-12-28 10:55:00', '192.168.1.123', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2023-12-27 15:15:00', '192.168.1.124', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'); + +-- 5. 插入用戶按讚記錄 (8條) - 使用默認用戶 +INSERT INTO user_likes (id, app_id, user_id, liked_at) VALUES +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2024-01-20 10:35:00'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2024-01-19 14:25:00'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2024-01-18 09:20:00'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2024-01-17 16:50:00'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2024-01-16 11:35:00'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2024-01-15 08:25:00'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2024-01-14 15:15:00'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2024-01-13 12:50:00'); + +-- 6. 插入用戶評分記錄 (1條) - 使用默認用戶,避免重複約束 +INSERT INTO user_ratings (id, app_id, user_id, rating, rated_at) VALUES +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, 5, '2024-01-20 10:40:00'); + +-- 7. 更新應用的統計數據 +UPDATE apps +SET + views_count = (SELECT COUNT(*) FROM user_views WHERE app_id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7'), + likes_count = (SELECT COUNT(*) FROM user_likes WHERE app_id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7'), + updated_at = NOW() +WHERE id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7'; + +-- 8. 顯示統計摘要 +SELECT + '統計摘要' as 項目, + (SELECT COUNT(*) FROM user_views WHERE app_id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7') as 總瀏覽數, + (SELECT COUNT(*) FROM user_likes WHERE app_id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7') as 總讚數, + (SELECT COUNT(*) FROM user_ratings WHERE app_id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7') as 總評分數, + (SELECT AVG(rating) FROM user_ratings WHERE app_id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7') as 平均評分; diff --git a/scripts/insert-test-data.sql b/scripts/insert-test-data.sql new file mode 100644 index 0000000..40a3c6d --- /dev/null +++ b/scripts/insert-test-data.sql @@ -0,0 +1,78 @@ +-- 插入真實測試數據到資料庫 +-- 請在 MySQL 中執行此腳本 + +-- 1. 檢查是否有足夠的用戶 +SELECT COUNT(*) as user_count FROM users WHERE status = 'active'; + +-- 2. 清空現有的測試數據(如果有的話) +DELETE FROM user_views WHERE app_id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7'; +DELETE FROM user_likes WHERE app_id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7'; +DELETE FROM user_ratings WHERE app_id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7'; + +-- 2. 插入用戶瀏覽記錄 (25條) - 使用不同的用戶避免重複約束 +INSERT INTO user_views (id, app_id, user_id, viewed_at, ip_address, user_agent) VALUES +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 0), '2024-01-20 10:30:00', '192.168.1.100', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 1), '2024-01-19 14:20:00', '192.168.1.101', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 2), '2024-01-18 09:15:00', '192.168.1.102', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 3), '2024-01-17 16:45:00', '192.168.1.103', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 4), '2024-01-16 11:30:00', '192.168.1.104', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 5), '2024-01-15 08:20:00', '192.168.1.105', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 6), '2024-01-14 15:10:00', '192.168.1.106', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 7), '2024-01-13 12:45:00', '192.168.1.107', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 8), '2024-01-12 17:30:00', '192.168.1.108', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 9), '2024-01-11 09:15:00', '192.168.1.109', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 0), '2024-01-10 14:25:00', '192.168.1.110', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 1), '2024-01-09 11:40:00', '192.168.1.111', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 2), '2024-01-08 16:55:00', '192.168.1.112', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 3), '2024-01-07 13:20:00', '192.168.1.113', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 4), '2024-01-06 10:35:00', '192.168.1.114', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 5), '2024-01-05 15:50:00', '192.168.1.115', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 6), '2024-01-04 12:15:00', '192.168.1.116', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 7), '2024-01-03 09:30:00', '192.168.1.117', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 8), '2024-01-02 17:45:00', '192.168.1.118', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 9), '2024-01-01 14:20:00', '192.168.1.119', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 0), '2023-12-31 11:10:00', '192.168.1.120', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 1), '2023-12-30 16:25:00', '192.168.1.121', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 2), '2023-12-29 13:40:00', '192.168.1.122', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 3), '2023-12-28 10:55:00', '192.168.1.123', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 4), '2023-12-27 15:15:00', '192.168.1.124', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'); + +-- 3. 插入用戶按讚記錄 (8條) - 使用不同的用戶避免重複約束 +INSERT INTO user_likes (id, app_id, user_id, liked_at) VALUES +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 0), '2024-01-20 10:35:00'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 1), '2024-01-19 14:25:00'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 2), '2024-01-18 09:20:00'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 3), '2024-01-17 16:50:00'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 4), '2024-01-16 11:35:00'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 5), '2024-01-15 08:25:00'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 6), '2024-01-14 15:15:00'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 7), '2024-01-13 12:50:00'); + +-- 4. 插入用戶評分記錄 (10條) - 使用不同的用戶避免重複約束 +INSERT INTO user_ratings (id, app_id, user_id, rating, rated_at) VALUES +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 0), 5, '2024-01-20 10:40:00'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 1), 4, '2024-01-19 14:30:00'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 2), 5, '2024-01-18 09:25:00'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 3), 3, '2024-01-17 16:55:00'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 4), 4, '2024-01-16 11:40:00'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 5), 5, '2024-01-15 08:30:00'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 6), 4, '2024-01-14 15:20:00'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 7), 5, '2024-01-13 12:55:00'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 8), 4, '2024-01-12 17:35:00'), +(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 9), 5, '2024-01-11 09:20:00'); + +-- 5. 更新應用的統計數據 +UPDATE apps +SET + views_count = (SELECT COUNT(*) FROM user_views WHERE app_id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7'), + likes_count = (SELECT COUNT(*) FROM user_likes WHERE app_id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7'), + updated_at = NOW() +WHERE id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7'; + +-- 6. 顯示統計摘要 +SELECT + '統計摘要' as 項目, + (SELECT COUNT(*) FROM user_views WHERE app_id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7') as 總瀏覽數, + (SELECT COUNT(*) FROM user_likes WHERE app_id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7') as 總讚數, + (SELECT COUNT(*) FROM user_ratings WHERE app_id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7') as 總評分數, + (SELECT AVG(rating) FROM user_ratings WHERE app_id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7') as 平均評分; diff --git a/scripts/simple-check.js b/scripts/simple-check.js new file mode 100644 index 0000000..5a686df --- /dev/null +++ b/scripts/simple-check.js @@ -0,0 +1,28 @@ +const mysql = require('mysql2/promise'); + +async function simpleCheck() { + try { + const connection = await mysql.createConnection({ + host: 'localhost', + user: 'root', + password: '123456', + database: 'ai_showcase_platform' + }); + + console.log('✅ 資料庫連接成功'); + + // 檢查 user_ratings 表 + const [ratings] = await connection.execute('SELECT * FROM user_ratings LIMIT 5'); + console.log('📊 user_ratings 數據:', ratings); + + // 檢查 apps 表 + const [apps] = await connection.execute('SELECT id, name FROM apps LIMIT 3'); + console.log('📱 apps 數據:', apps); + + await connection.end(); + } catch (error) { + console.error('❌ 錯誤:', error.message); + } +} + +simpleCheck();