新增資料庫、用戶註冊、登入的功能

This commit is contained in:
2025-08-05 10:56:22 +08:00
parent 94e3763402
commit a288a966ba
41 changed files with 4362 additions and 289 deletions

109
BACKEND_STAGE1_REPORT.md Normal file
View File

@@ -0,0 +1,109 @@
# AI Showcase Platform - Backend Stage 1 完成報告
## ✅ 第一階段功能清單
- [x] .env 檔案配置
- [x] Next.js API Routes 架構
- [x] CORS/中間件/錯誤處理
- [x] 日誌系統lib/logger.ts
- [x] 認證與授權系統JWT, bcrypt, 角色權限)
- [x] 登入/登出 API
- [x] 密碼加密與驗證
- [x] 角色權限控制 (user/developer/admin)
- [x] 密碼重設 API含驗證碼流程
- [x] 用戶註冊 API
- [x] 用戶資料查詢/更新 API
- [x] 用戶列表 API管理員用
- [x] 用戶統計 API
---
## 🛠️ 主要 API 路徑
| 路徑 | 方法 | 權限 | 說明 |
|------|------|------|------|
| `/api` | GET | 公開 | 健康檢查 |
| `/api/auth/register` | POST | 公開 | 用戶註冊 |
| `/api/auth/login` | POST | 公開 | 用戶登入(回傳 JWT |
| `/api/auth/me` | GET | 登入 | 取得當前用戶資料 |
| `/api/auth/reset-password/request` | POST | 公開 | 密碼重設請求(產生驗證碼) |
| `/api/auth/reset-password/confirm` | POST | 公開 | 密碼重設確認(驗證碼+新密碼) |
| `/api/users` | GET | 管理員 | 用戶列表(分頁) |
| `/api/users/stats` | GET | 管理員 | 用戶統計資料 |
---
## 👤 測試用帳號
- 管理員帳號:
- Email: `admin@theaken.com`
- 密碼: `Admin@2025`(已重設)
- 角色: `admin`
- 測試用戶:
- Email: `test@theaken.com` / `test@example.com`
- 密碼: `Test@2024`
- 角色: `user`
---
## 🧪 自動化測試腳本與結果
### 1. 健康檢查 API
```
GET /api
→ 200 OK
{"message":"AI Platform API is running", ...}
```
### 2. 註冊 API
```
POST /api/auth/register { name, email, password, department }
→ 409 已註冊(重複測試)
```
### 3. 登入 API
```
POST /api/auth/login { email, password }
→ 200 OK, 回傳 JWT
```
### 4. 取得當前用戶
```
GET /api/auth/me (需 JWT)
→ 200 OK, 回傳用戶資料
```
### 5. 用戶列表(管理員)
```
GET /api/users (需管理員 JWT)
→ 200 OK, 回傳用戶列表與分頁
```
### 6. 密碼重設流程
```
POST /api/auth/reset-password/request { email }
→ 200 OK, 回傳驗證碼
POST /api/auth/reset-password/confirm { email, code, newPassword }
→ 200 OK, 密碼重設成功
```
### 7. 用戶統計
```
GET /api/users/stats (需管理員 JWT)
→ 200 OK, { total, admin, developer, user, today }
```
---
## 📝 測試結果摘要
- 所有 API 路徑皆可正常運作,權限驗證正確。
- 密碼重設流程可用(驗證碼測試用直接回傳)。
- 用戶列表、統計、註冊、登入、查詢皆通過。
- 日誌系統可記錄 API 請求與錯誤。
---
> 本報告可作為雙方確認第一階段後端功能完成度與測試依據。
> 完成後可刪除本 MD。

View File

@@ -45,7 +45,7 @@ const [isLoading, setIsLoading] = useState(false) // 載入狀態
### 2.3 API整合 ### 2.3 API整合
```typescript ```typescript
// DeepSeek API 配置 // DeepSeek API 配置
const DEEPSEEK_API_KEY = "sk-3640dcff23fe4a069a64f536ac538d75" const DEEPSEEK_API_KEY = "your_api_key_here"
const DEEPSEEK_API_URL = "https://api.deepseek.com/v1/chat/completions" const DEEPSEEK_API_URL = "https://api.deepseek.com/v1/chat/completions"
// API 調用函數 // API 調用函數

379
DATABASE_GUIDE.md Normal file
View File

@@ -0,0 +1,379 @@
# 🗄️ AI展示平台資料庫指南
## 📋 資料庫概述
AI展示平台使用 **MySQL** 作為主要資料庫支援完整的競賽管理、用戶認證、評審系統和AI助手功能。
### 🔗 連接資訊
- **主機**: mysql.theaken.com
- **埠號**: 33306
- **資料庫**: db_AI_Platform
- **用戶**: AI_Platform
- **密碼**: Aa123456
## 🏗️ 資料庫結構
### 📊 核心資料表 (18個)
#### 1. 用戶管理
- **users** - 用戶基本資料
- **user_favorites** - 用戶收藏
- **user_likes** - 用戶按讚記錄
#### 2. 競賽系統
- **competitions** - 競賽基本資料
- **competition_participants** - 競賽參與者
- **competition_judges** - 競賽評審分配
#### 3. 團隊管理
- **teams** - 團隊基本資料
- **team_members** - 團隊成員
#### 4. 作品管理
- **apps** - AI應用程式
- **proposals** - 提案作品
#### 5. 評審系統
- **judges** - 評審基本資料
- **judge_scores** - 評審評分
#### 6. 獎項系統
- **awards** - 獎項記錄
#### 7. AI助手
- **chat_sessions** - 聊天會話
- **chat_messages** - 聊天訊息
- **ai_assistant_configs** - AI配置
#### 8. 系統管理
- **system_settings** - 系統設定
- **activity_logs** - 活動日誌
## 🚀 快速開始
### 1. 環境設定
```bash
# 複製環境變數範例
cp env.example .env.local
# 編輯環境變數
nano .env.local
```
### 2. 安裝依賴
```bash
# 安裝新依賴
pnpm install
```
### 3. 建立資料庫
```bash
# 建立資料庫和資料表
pnpm run db:setup
```
### 4. 測試連接
```bash
# 測試資料庫連接
pnpm run db:test
```
## 📊 資料表詳細說明
### users 表
```sql
CREATE TABLE users (
id VARCHAR(36) PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
avatar VARCHAR(500),
department VARCHAR(100) NOT NULL,
role ENUM('user', 'developer', 'admin') DEFAULT 'user',
join_date DATE NOT NULL,
total_likes INT DEFAULT 0,
total_views INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
```
**用途**: 儲存所有用戶資料
**角色**:
- `user`: 一般用戶 (瀏覽、投票)
- `developer`: 開發者 (提交作品、參賽)
- `admin`: 管理員 (系統管理、數據分析)
### competitions 表
```sql
CREATE TABLE competitions (
id VARCHAR(36) PRIMARY KEY,
name VARCHAR(200) NOT NULL,
year INT NOT NULL,
month INT NOT NULL,
start_date DATE NOT NULL,
end_date DATE NOT NULL,
status ENUM('upcoming', 'active', 'judging', 'completed') DEFAULT 'upcoming',
description TEXT,
type ENUM('individual', 'team', 'mixed', 'proposal') NOT NULL,
evaluation_focus TEXT,
max_team_size INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
```
**競賽狀態流程**: `upcoming``active``judging``completed`
**競賽類型**:
- `individual`: 個人賽
- `team`: 團隊賽
- `mixed`: 混合賽
- `proposal`: 提案賽
### judge_scores 表
```sql
CREATE TABLE judge_scores (
id VARCHAR(36) PRIMARY KEY,
judge_id VARCHAR(36) NOT NULL,
app_id VARCHAR(36),
proposal_id VARCHAR(36),
scores JSON NOT NULL,
comments TEXT,
submitted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
```
**評分維度** (JSON格式):
```json
{
"innovation": 8, // 創新性 (1-10)
"technical": 7, // 技術性 (1-10)
"usability": 9, // 實用性 (1-10)
"presentation": 8, // 展示效果 (1-10)
"impact": 7 // 影響力 (1-10)
}
```
## 🔍 查詢範例
### 1. 獲取用戶統計
```sql
SELECT
u.name,
u.department,
u.role,
COUNT(DISTINCT a.id) as total_apps,
COUNT(DISTINCT f.app_id) as total_favorites,
u.total_likes,
u.total_views
FROM users u
LEFT JOIN apps a ON u.id = a.creator_id
LEFT JOIN user_favorites f ON u.id = f.user_id
GROUP BY u.id;
```
### 2. 獲取競賽統計
```sql
SELECT
c.name,
c.status,
c.type,
COUNT(DISTINCT cp.user_id) as participant_count,
COUNT(DISTINCT cp.team_id) as team_count,
COUNT(DISTINCT cp.app_id) as app_count
FROM competitions c
LEFT JOIN competition_participants cp ON c.id = cp.competition_id
GROUP BY c.id;
```
### 3. 獲取應用排行榜
```sql
SELECT
a.name,
u.name as creator_name,
t.name as team_name,
a.likes_count,
a.views_count,
a.rating,
ROW_NUMBER() OVER (ORDER BY a.likes_count DESC) as popularity_rank
FROM apps a
LEFT JOIN users u ON a.creator_id = u.id
LEFT JOIN teams t ON a.team_id = t.id;
```
## 🛠️ 存儲過程
### GetUserPermissions
```sql
CALL GetUserPermissions('user@example.com');
```
**用途**: 獲取用戶權限和基本資料
### GetCompetitionStats
```sql
CALL GetCompetitionStats('comp-2025-01');
```
**用途**: 獲取競賽統計資料
### CalculateAwardRankings
```sql
CALL CalculateAwardRankings('comp-2025-01');
```
**用途**: 計算獎項排名
## 👁️ 視圖 (Views)
### user_statistics
顯示用戶統計資料,包含作品數、收藏數、按讚數等
### competition_statistics
顯示競賽統計資料,包含參與者數、團隊數、作品數等
### app_rankings
顯示應用排行榜,包含人氣排名和評分排名
## 🔧 觸發器 (Triggers)
### update_user_total_likes
當用戶按讚時,自動更新用戶總按讚數
### update_app_likes_count
當應用被按讚時,自動更新應用按讚數
### update_user_total_views
當應用瀏覽數更新時,自動更新用戶總瀏覽數
## 📈 索引優化
### 主要索引
- `users.email` - 用戶郵箱查詢
- `users.role` - 角色權限查詢
- `competitions.status` - 競賽狀態查詢
- `apps.likes_count` - 人氣排序
- `apps.rating` - 評分排序
### 複合索引
- `competitions.year, competitions.month` - 時間範圍查詢
- `competitions.start_date, competitions.end_date` - 日期範圍查詢
## 🔒 安全性
### 密碼加密
使用 bcrypt 進行密碼雜湊,鹽值輪數為 10
### 外鍵約束
所有關聯表都設定了適當的外鍵約束,確保資料完整性
### 唯一約束
- 用戶郵箱唯一
- 用戶每日按讚限制
- 評審對同一作品只能評分一次
## 📊 性能監控
### 查詢統計
```sql
-- 查看慢查詢
SHOW VARIABLES LIKE 'slow_query_log';
SHOW VARIABLES LIKE 'long_query_time';
-- 查看連接數
SHOW STATUS LIKE 'Threads_connected';
SHOW STATUS LIKE 'Max_used_connections';
```
### 資料表大小
```sql
SELECT
table_name,
ROUND(((data_length + index_length) / 1024 / 1024), 2) AS 'Size (MB)'
FROM information_schema.tables
WHERE table_schema = 'db_AI_Platform'
ORDER BY (data_length + index_length) DESC;
```
## 🚨 故障排除
### 常見問題
#### 1. 連接失敗
```bash
# 檢查網路連接
ping mysql.theaken.com
# 檢查埠號
telnet mysql.theaken.com 33306
```
#### 2. 權限錯誤
```sql
-- 檢查用戶權限
SHOW GRANTS FOR 'AI_Platform'@'%';
```
#### 3. 資料表不存在
```bash
# 重新執行建立腳本
pnpm run db:setup
```
#### 4. 密碼錯誤
```bash
# 檢查環境變數
echo $DB_PASSWORD
```
## 📝 維護指南
### 定期備份
```bash
# 建立備份
mysqldump -h mysql.theaken.com -P 33306 -u AI_Platform -p db_AI_Platform > backup_$(date +%Y%m%d).sql
```
### 資料清理
```sql
-- 清理過期的活動日誌 (保留30天)
DELETE FROM activity_logs WHERE created_at < DATE_SUB(NOW(), INTERVAL 30 DAY);
-- 清理過期的聊天會話 (保留7天)
DELETE FROM chat_sessions WHERE updated_at < DATE_SUB(NOW(), INTERVAL 7 DAY);
```
### 性能優化
```sql
-- 分析資料表
ANALYZE TABLE users, competitions, apps;
-- 優化資料表
OPTIMIZE TABLE users, competitions, apps;
```
## 🔄 版本更新
### 新增欄位
```sql
-- 範例:為 users 表新增欄位
ALTER TABLE users ADD COLUMN phone VARCHAR(20) AFTER email;
```
### 修改欄位
```sql
-- 範例:修改欄位類型
ALTER TABLE users MODIFY COLUMN department VARCHAR(150);
```
### 新增索引
```sql
-- 範例:新增複合索引
CREATE INDEX idx_user_department_role ON users(department, role);
```
---
**最後更新**: 2025年1月
**維護者**: AI展示平台開發團隊

186
DATABASE_SETUP_COMPLETE.md Normal file
View File

@@ -0,0 +1,186 @@
# 🎉 AI展示平台資料庫建立完成
## 📊 建立結果總結
### ✅ 成功建立的資料表 (18個)
| 序號 | 資料表名稱 | 狀態 | 記錄數 |
|------|------------|------|--------|
| 1 | users | ✅ | 1 |
| 2 | competitions | ✅ | 2 |
| 3 | judges | ✅ | 3 |
| 4 | teams | ✅ | 0 |
| 5 | team_members | ✅ | 0 |
| 6 | apps | ✅ | 0 |
| 7 | proposals | ✅ | 0 |
| 8 | judge_scores | ✅ | 0 |
| 9 | awards | ✅ | 0 |
| 10 | chat_sessions | ✅ | 0 |
| 11 | chat_messages | ✅ | 0 |
| 12 | ai_assistant_configs | ✅ | 1 |
| 13 | user_favorites | ✅ | 0 |
| 14 | user_likes | ✅ | 0 |
| 15 | competition_participants | ✅ | 0 |
| 16 | competition_judges | ✅ | 0 |
| 17 | system_settings | ✅ | 8 |
| 18 | activity_logs | ✅ | 0 |
### 📈 初始數據統計
- **管理員用戶**: 1 筆 (admin@theaken.com)
- **預設評審**: 3 筆 (張教授、李經理、王工程師)
- **預設競賽**: 2 筆 (2025年AI創新競賽、2025年提案競賽)
- **AI助手配置**: 1 筆
- **系統設定**: 8 筆 (包含各種系統參數)
## 🔗 資料庫連接資訊
- **主機**: mysql.theaken.com
- **埠號**: 33306
- **資料庫**: db_AI_Platform
- **用戶**: AI_Platform
- **密碼**: Aa123456
- **MySQL版本**: 9.3.0
## 🛠️ 建立的腳本文件
1. **`database_setup.sql`** - 完整版SQL腳本 (包含觸發器和存儲過程)
2. **`database_setup_simple.sql`** - 簡化版SQL腳本 (僅基本資料表)
3. **`scripts/setup-database.js`** - 自動化建立腳本
4. **`scripts/setup-database-manual.js`** - 手動建立腳本
5. **`scripts/fix-tables.js`** - 修復資料表腳本
6. **`scripts/fix-user-likes.js`** - 修復user_likes表腳本
7. **`database_connection_test.js`** - 連接測試腳本
8. **`lib/database.ts`** - 資料庫操作工具類
## 📋 可用的npm腳本
```bash
# 建立資料庫
pnpm run db:setup
# 測試連接
pnpm run db:test
# 手動建立 (推薦)
node scripts/setup-database-manual.js
# 修復資料表
node scripts/fix-tables.js
```
## 🔧 資料庫功能特色
### 🏗️ 完整的資料結構
- **18個核心資料表** 支援所有平台功能
- **完整的外鍵約束** 確保資料完整性
- **優化的索引設計** 提升查詢效能
- **JSON欄位支援** 儲存複雜資料結構
### 🔒 安全性設計
- **密碼加密**: 使用bcrypt進行密碼雜湊
- **唯一約束**: 防止重複資料
- **外鍵約束**: 確保資料關聯完整性
- **索引優化**: 提升查詢效能
### 📊 初始數據
- **預設管理員**: admin@theaken.com (密碼: admin123)
- **預設評審**: 3位不同專業領域的評審
- **預設競賽**: 2個不同類型的競賽
- **系統設定**: 8個核心系統參數
## 🚀 下一步開發計劃
### 1. 後端API開發
```bash
# 建議的API端點
/api/auth/login # 用戶登入
/api/auth/register # 用戶註冊
/api/competitions # 競賽管理
/api/users # 用戶管理
/api/judges # 評審管理
/api/apps # 應用管理
/api/teams # 團隊管理
/api/awards # 獎項管理
```
### 2. 前端整合
```bash
# 替換Mock數據
- 更新 auth-context.tsx 使用真實API
- 更新 competition-context.tsx 使用真實API
- 實現真實的用戶認證
- 連接資料庫進行CRUD操作
```
### 3. 環境配置
```bash
# 複製環境變數
cp env.example .env.local
# 編輯環境變數
nano .env.local
```
## 📝 使用指南
### 1. 連接資料庫
```typescript
import { db } from '@/lib/database'
// 查詢用戶
const users = await db.query('SELECT * FROM users')
// 插入數據
const userId = await db.insert('users', {
id: 'user-001',
name: '測試用戶',
email: 'test@example.com',
password_hash: 'hashed_password',
department: '技術部',
role: 'user',
join_date: '2025-01-01'
})
```
### 2. 用戶認證
```typescript
// 登入驗證
const user = await db.queryOne(
'SELECT * FROM users WHERE email = ? AND password_hash = ?',
[email, hashedPassword]
)
```
### 3. 競賽管理
```typescript
// 獲取競賽列表
const competitions = await db.query(
'SELECT * FROM competitions ORDER BY created_at DESC'
)
```
## 🎯 專案狀態
-**資料庫設計**: 完成
-**資料表建立**: 完成
-**初始數據**: 完成
-**連接測試**: 完成
- 🔄 **後端API**: 待開發
- 🔄 **前端整合**: 待開發
- 🔄 **部署配置**: 待開發
## 📞 技術支援
如果遇到問題,請檢查:
1. **連接問題**: 確認主機、埠號、用戶名、密碼
2. **權限問題**: 確認用戶有足夠的資料庫權限
3. **語法錯誤**: 檢查SQL語句語法
4. **依賴問題**: 確認已安裝所有必要依賴
---
**建立時間**: 2025年1月
**建立者**: AI展示平台開發團隊
**狀態**: ✅ 完成

View File

@@ -74,8 +74,7 @@ ai-showcase-platform/
├── app/ # Next.js App Router ├── app/ # Next.js App Router
│ ├── admin/ # 管理員頁面 │ ├── admin/ # 管理員頁面
│ │ ├── page.tsx # 管理員主頁 │ │ ├── page.tsx # 管理員主頁
│ │ ── scoring/ # 評分管理 │ │ ── scoring/ # 評分管理
│ │ └── scoring-test/ # 評分測試
│ ├── competition/ # 競賽頁面 │ ├── competition/ # 競賽頁面
│ ├── judge-scoring/ # 評審評分頁面 │ ├── judge-scoring/ # 評審評分頁面
│ ├── register/ # 註冊頁面 │ ├── register/ # 註冊頁面

73
SECURITY_CHECKLIST.md Normal file
View File

@@ -0,0 +1,73 @@
# 🔒 安全檢查清單
## ✅ 已清理的敏感資訊
### 1. 測試帳號資訊
- ✅ 移除 `components/auth/login-dialog.tsx` 中的測試帳號顯示
- ✅ 清理測試帳號:`zhang@panjit.com`
- ✅ 清理測試密碼:`password123`
### 2. API 金鑰
- ✅ 移除 `components/chat-bot.tsx` 中的硬編碼 API 金鑰
- ✅ 清理 `CHATBOT_ANALYSIS.md` 中的示例 API 金鑰
- ✅ 使用環境變數管理 API 金鑰
### 3. 測試頁面
- ✅ 刪除 `app/admin/scoring-test/page.tsx`
- ✅ 刪除 `app/admin/scoring-form-test/page.tsx`
- ✅ 更新 README.md 中的目錄結構
### 4. 測試功能
- ✅ 修改 `components/admin/system-settings.tsx` 中的測試郵件功能
- ✅ 添加安全註釋到認證邏輯中
## 🔍 安全檢查項目
### 環境變數
- [ ] 確保 `.env.local` 檔案已加入 `.gitignore`
- [ ] 檢查是否有硬編碼的 API 金鑰
- [ ] 驗證所有敏感資訊都使用環境變數
### 認證系統
- [ ] 生產環境應使用真實的認證服務
- [ ] 移除所有測試帳號和密碼
- [ ] 實施適當的密碼加密
### 代碼安全
- [ ] 移除所有測試和調試代碼
- [ ] 檢查是否有敏感資訊洩露
- [ ] 確保錯誤訊息不包含敏感資訊
## 🚨 生產環境注意事項
1. **API 金鑰管理**
- 使用環境變數存儲所有 API 金鑰
- 定期輪換 API 金鑰
- 監控 API 使用情況
2. **認證系統**
- 實施真實的用戶認證
- 使用安全的密碼加密
- 實施適當的會話管理
3. **數據安全**
- 加密敏感數據
- 實施適當的訪問控制
- 定期備份數據
4. **監控和日誌**
- 實施安全事件監控
- 記錄所有認證嘗試
- 監控異常活動
## 📝 更新記錄
- **2025-01-XX**: 初始安全清理
- 移除測試帳號資訊
- 清理硬編碼 API 金鑰
- 刪除測試頁面
- 更新文檔
---
**注意**: 此清單應定期更新,確保系統安全性。

View File

@@ -1,195 +0,0 @@
"use client"
import { useState } from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { CheckCircle, Edit, Loader2 } from "lucide-react"
export default function ScoringFormTestPage() {
const [showScoringForm, setShowScoringForm] = useState(false)
const [manualScoring, setManualScoring] = useState({
judgeId: "judge1",
participantId: "app1",
scores: {
"創新性": 0,
"技術性": 0,
"實用性": 0,
"展示效果": 0,
"影響力": 0
},
comments: ""
})
const [isLoading, setIsLoading] = useState(false)
const scoringRules = [
{ name: "創新性", description: "技術創新程度和獨特性", weight: 25 },
{ name: "技術性", description: "技術實現的複雜度和穩定性", weight: 20 },
{ name: "實用性", description: "實際應用價值和用戶體驗", weight: 25 },
{ name: "展示效果", description: "演示效果和表達能力", weight: 15 },
{ name: "影響力", description: "對行業和社會的潛在影響", weight: 15 }
]
const calculateTotalScore = (scores: Record<string, number>): number => {
let totalScore = 0
let totalWeight = 0
scoringRules.forEach(rule => {
const score = scores[rule.name] || 0
const weight = rule.weight || 1
totalScore += score * weight
totalWeight += weight
})
return totalWeight > 0 ? Math.round(totalScore / totalWeight) : 0
}
const handleSubmitScore = async () => {
setIsLoading(true)
// 模擬提交
setTimeout(() => {
setIsLoading(false)
setShowScoringForm(false)
}, 2000)
}
return (
<div className="container mx-auto py-6">
<div className="mb-6">
<h1 className="text-3xl font-bold"></h1>
<p className="text-gray-600"></p>
</div>
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<Button onClick={() => setShowScoringForm(true)} size="lg">
<Edit className="w-5 h-5 mr-2" />
</Button>
</CardContent>
</Card>
<Dialog open={showScoringForm} onOpenChange={setShowScoringForm}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center space-x-2">
<Edit className="w-5 h-5" />
<span></span>
</DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
<div className="space-y-6">
{/* 評分項目 */}
<div className="space-y-4">
<h3 className="text-lg font-semibold"></h3>
{scoringRules.map((rule, index) => (
<div key={index} className="space-y-4 p-6 border rounded-lg bg-white shadow-sm">
<div className="flex justify-between items-start">
<div className="flex-1">
<Label className="text-lg font-semibold text-gray-900">{rule.name}</Label>
<p className="text-sm text-gray-600 mt-2 leading-relaxed">{rule.description}</p>
<p className="text-xs text-purple-600 mt-2 font-medium">{rule.weight}%</p>
</div>
<div className="text-right ml-4">
<span className="text-2xl font-bold text-blue-600">
{manualScoring.scores[rule.name] || 0} / 10
</span>
</div>
</div>
{/* 評分按鈕 */}
<div className="flex flex-wrap gap-3">
{Array.from({ length: 10 }, (_, i) => i + 1).map((score) => (
<button
key={score}
type="button"
onClick={() => setManualScoring({
...manualScoring,
scores: { ...manualScoring.scores, [rule.name]: score }
})}
className={`w-12 h-12 rounded-lg border-2 font-semibold text-lg transition-all duration-200 ${
(manualScoring.scores[rule.name] || 0) === score
? 'bg-blue-600 text-white border-blue-600 shadow-lg scale-105'
: 'bg-white text-gray-700 border-gray-300 hover:border-blue-400 hover:bg-blue-50 hover:scale-105'
}`}
>
{score}
</button>
))}
</div>
</div>
))}
</div>
{/* 總分顯示 */}
<div className="p-6 bg-gradient-to-r from-blue-50 to-purple-50 rounded-lg border-2 border-blue-200">
<div className="flex justify-between items-center">
<div>
<span className="text-xl font-bold text-gray-900"></span>
<p className="text-sm text-gray-600 mt-1"></p>
</div>
<div className="flex items-center space-x-3">
<span className="text-4xl font-bold text-blue-600">
{calculateTotalScore(manualScoring.scores)}
</span>
<span className="text-xl text-gray-500 font-medium">/ 10</span>
</div>
</div>
</div>
{/* 評審意見 */}
<div className="space-y-3">
<Label className="text-lg font-semibold"> *</Label>
<Textarea
placeholder="請詳細填寫評審意見、優點分析、改進建議等..."
value={manualScoring.comments}
onChange={(e) => setManualScoring({ ...manualScoring, comments: e.target.value })}
rows={6}
className="min-h-[120px] resize-none"
/>
<p className="text-xs text-gray-500"></p>
</div>
</div>
<div className="flex justify-end space-x-4 pt-6 border-t border-gray-200">
<Button
variant="outline"
size="lg"
onClick={() => setShowScoringForm(false)}
className="px-8"
>
</Button>
<Button
onClick={handleSubmitScore}
disabled={isLoading}
size="lg"
className="px-8 bg-blue-600 hover:bg-blue-700"
>
{isLoading ? (
<>
<Loader2 className="w-5 h-5 mr-2 animate-spin" />
...
</>
) : (
<>
<CheckCircle className="w-5 h-5 mr-2" />
</>
)}
</Button>
</div>
</DialogContent>
</Dialog>
</div>
)
}

View File

@@ -1,13 +0,0 @@
import { ScoringManagement } from "@/components/admin/scoring-management"
export default function ScoringTestPage() {
return (
<div className="container mx-auto py-6">
<div className="mb-6">
<h1 className="text-3xl font-bold"></h1>
<p className="text-gray-600"></p>
</div>
<ScoringManagement />
</div>
)
}

115
app/api/auth/login/route.ts Normal file
View File

@@ -0,0 +1,115 @@
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/database';
import { generateToken, validatePassword, comparePassword } from '@/lib/auth';
import { logger } from '@/lib/logger';
export async function POST(request: NextRequest) {
const startTime = Date.now();
try {
const body = await request.json();
const { email, password } = body;
// 驗證輸入
if (!email || !password) {
return NextResponse.json(
{ error: '請提供電子郵件和密碼' },
{ status: 400 }
);
}
// 驗證密碼格式
const passwordValidation = await validatePassword(password);
if (!passwordValidation.isValid) {
return NextResponse.json(
{ error: '密碼格式不正確', details: passwordValidation.errors },
{ status: 400 }
);
}
// 查詢用戶
const user = await db.queryOne<{
id: string;
name: string;
email: string;
password_hash: string;
avatar?: string;
department: string;
role: 'user' | 'developer' | 'admin';
join_date: string;
total_likes: number;
total_views: number;
created_at: string;
updated_at: string;
}>(
'SELECT * FROM users WHERE email = ?',
[email]
);
if (!user) {
logger.logAuth('login', email, false, request.ip || 'unknown');
return NextResponse.json(
{ error: '電子郵件或密碼不正確' },
{ status: 401 }
);
}
// 驗證密碼
const isPasswordValid = await comparePassword(password, user.password_hash);
if (!isPasswordValid) {
logger.logAuth('login', email, false, request.ip || 'unknown');
return NextResponse.json(
{ error: '電子郵件或密碼不正確' },
{ status: 401 }
);
}
// 生成 JWT Token
const token = generateToken({
id: user.id,
email: user.email,
role: user.role
});
// 更新最後登入時間
await db.update(
'users',
{ updated_at: new Date().toISOString().slice(0, 19).replace('T', ' ') },
{ id: user.id }
);
// 記錄成功登入
logger.logAuth('login', email, true, request.ip || 'unknown');
const duration = Date.now() - startTime;
logger.logRequest('POST', '/api/auth/login', 200, duration, user.id);
return NextResponse.json({
message: '登入成功',
user: {
id: user.id,
name: user.name,
email: user.email,
avatar: user.avatar,
department: user.department,
role: user.role,
joinDate: user.join_date,
totalLikes: user.total_likes,
totalViews: user.total_views
},
token,
expiresIn: process.env.JWT_EXPIRES_IN || '7d'
});
} catch (error) {
logger.logError(error as Error, 'Login API');
const duration = Date.now() - startTime;
logger.logRequest('POST', '/api/auth/login', 500, duration);
return NextResponse.json(
{ error: '內部伺服器錯誤' },
{ status: 500 }
);
}
}

47
app/api/auth/me/route.ts Normal file
View File

@@ -0,0 +1,47 @@
import { NextRequest, NextResponse } from 'next/server';
import { authenticateUser } from '@/lib/auth';
import { logger } from '@/lib/logger';
export async function GET(request: NextRequest) {
const startTime = Date.now();
try {
// 驗證用戶
const user = await authenticateUser(request);
if (!user) {
return NextResponse.json(
{ error: '未授權訪問' },
{ status: 401 }
);
}
const duration = Date.now() - startTime;
logger.logRequest('GET', '/api/auth/me', 200, duration, user.id);
return NextResponse.json({
user: {
id: user.id,
name: user.name,
email: user.email,
avatar: user.avatar,
department: user.department,
role: user.role,
joinDate: user.joinDate,
totalLikes: user.totalLikes,
totalViews: user.totalViews
}
});
} catch (error) {
logger.logError(error as Error, 'Get Current User API');
const duration = Date.now() - startTime;
logger.logRequest('GET', '/api/auth/me', 500, duration);
return NextResponse.json(
{ error: '內部伺服器錯誤' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,113 @@
import { NextRequest, NextResponse } from 'next/server';
import { db, generateId } from '@/lib/database';
import { validateUserData, validatePassword, hashPassword } from '@/lib/auth';
import { logger } from '@/lib/logger';
export async function POST(request: NextRequest) {
const startTime = Date.now();
try {
console.log('開始處理註冊請求...');
const body = await request.json();
console.log('請求體:', body);
const { name, email, password, department, role = 'user' } = body;
// 驗證用戶資料
console.log('驗證用戶資料...');
const userValidation = validateUserData({ name, email, department, role });
if (!userValidation.isValid) {
console.log('用戶資料驗證失敗:', userValidation.errors);
return NextResponse.json(
{ error: '用戶資料驗證失敗', details: userValidation.errors },
{ status: 400 }
);
}
// 驗證密碼
console.log('驗證密碼...');
const passwordValidation = await validatePassword(password);
if (!passwordValidation.isValid) {
console.log('密碼驗證失敗:', passwordValidation.errors);
return NextResponse.json(
{ error: '密碼格式不正確', details: passwordValidation.errors },
{ status: 400 }
);
}
// 檢查電子郵件是否已存在
console.log('檢查電子郵件是否已存在...');
const existingUser = await db.queryOne(
'SELECT id FROM users WHERE email = ?',
[email]
);
if (existingUser) {
console.log('電子郵件已存在');
return NextResponse.json(
{ error: '此電子郵件地址已被註冊' },
{ status: 409 }
);
}
// 加密密碼
console.log('加密密碼...');
const passwordHash = await hashPassword(password);
console.log('密碼加密完成');
// 準備用戶資料
console.log('準備用戶資料...');
const userId = generateId();
const userData = {
id: userId,
name: name.trim(),
email: email.toLowerCase().trim(),
password_hash: passwordHash,
department: department.trim(),
role,
join_date: new Date().toISOString().split('T')[0],
total_likes: 0,
total_views: 0,
created_at: new Date().toISOString().slice(0, 19).replace('T', ' '),
updated_at: new Date().toISOString().slice(0, 19).replace('T', ' ')
};
console.log('插入用戶資料...');
// 插入用戶資料
await db.insert('users', userData);
console.log('用戶資料插入成功');
// 記錄註冊成功
logger.logAuth('register', email, true, 'unknown');
const duration = Date.now() - startTime;
logger.logRequest('POST', '/api/auth/register', 201, duration, userId);
return NextResponse.json({
message: '註冊成功',
user: {
id: userData.id,
name: userData.name,
email: userData.email,
department: userData.department,
role: userData.role,
joinDate: userData.join_date,
totalLikes: userData.total_likes,
totalViews: userData.total_views
}
}, { status: 201 });
} catch (error) {
console.error('註冊 API 錯誤:', error);
logger.logError(error as Error, 'Register API');
const duration = Date.now() - startTime;
logger.logRequest('POST', '/api/auth/register', 500, duration);
return NextResponse.json(
{ error: '內部伺服器錯誤', details: error instanceof Error ? error.message : 'Unknown error' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,19 @@
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/database';
import { hashPassword } from '@/lib/auth';
import { codeMap } from '../request/route';
export async function POST(request: NextRequest) {
try {
const { email, code, newPassword } = await request.json();
if (!email || !code || !newPassword) return NextResponse.json({ error: '缺少參數' }, { status: 400 });
const validCode = codeMap.get(email);
if (!validCode || validCode !== code) return NextResponse.json({ error: '驗證碼錯誤' }, { status: 400 });
const passwordHash = await hashPassword(newPassword);
await db.update('users', { password_hash: passwordHash }, { email });
codeMap.delete(email);
return NextResponse.json({ message: '密碼重設成功' });
} catch (error) {
return NextResponse.json({ error: '內部伺服器錯誤', details: error instanceof Error ? error.message : 'Unknown error' }, { status: 500 });
}
}

View File

@@ -0,0 +1,20 @@
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/database';
const codeMap = new Map();
export async function POST(request: NextRequest) {
try {
const { email } = await request.json();
if (!email) return NextResponse.json({ error: '請提供 email' }, { status: 400 });
const user = await db.queryOne('SELECT id FROM users WHERE email = ?', [email]);
if (!user) return NextResponse.json({ error: '用戶不存在' }, { status: 404 });
const code = Math.floor(100000 + Math.random() * 900000).toString();
codeMap.set(email, code);
// 實際應發送 email這裡直接回傳
return NextResponse.json({ message: '驗證碼已產生', code });
} catch (error) {
return NextResponse.json({ error: '內部伺服器錯誤', details: error instanceof Error ? error.message : 'Unknown error' }, { status: 500 });
}
}
export { codeMap };

35
app/api/route.ts Normal file
View File

@@ -0,0 +1,35 @@
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/database';
export async function GET(request: NextRequest) {
try {
// 健康檢查
const isHealthy = await db.healthCheck();
if (!isHealthy) {
return NextResponse.json(
{ error: 'Database connection failed' },
{ status: 503 }
);
}
// 獲取基本統計
const stats = await db.getDatabaseStats();
return NextResponse.json({
message: 'AI Platform API is running',
version: '1.0.0',
timestamp: new Date().toISOString(),
database: {
status: 'connected',
stats
}
});
} catch (error) {
console.error('API Health Check Error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}

47
app/api/users/route.ts Normal file
View File

@@ -0,0 +1,47 @@
import { NextRequest, NextResponse } from 'next/server';
import { requireAdmin } from '@/lib/auth';
import { db } from '@/lib/database';
import { logger } from '@/lib/logger';
export async function GET(request: NextRequest) {
const startTime = Date.now();
try {
// 驗證管理員權限
const admin = await requireAdmin(request);
// 查詢參數
const { searchParams } = new URL(request.url);
const page = Math.max(1, parseInt(searchParams.get('page') || '1', 10));
const limit = Math.max(1, Math.min(100, parseInt(searchParams.get('limit') || '20', 10)));
const offset = (page - 1) * limit;
// 查詢用戶總數
const countResult = await db.queryOne<{ total: number }>('SELECT COUNT(*) as total FROM users');
const total = countResult?.total || 0;
// 查詢用戶列表
const users = await db.query(
`SELECT id, name, email, avatar, department, role, join_date, total_likes, total_views, created_at, updated_at FROM users ORDER BY created_at DESC LIMIT ${limit} OFFSET ${offset}`
);
// 分頁資訊
const totalPages = Math.ceil(total / limit);
const hasNext = page < totalPages;
const hasPrev = page > 1;
return NextResponse.json({
users: users.map(user => ({
id: user.id,
name: user.name,
email: user.email,
avatar: user.avatar,
department: user.department,
role: user.role,
joinDate: user.join_date,
totalLikes: user.total_likes,
totalViews: user.total_views,
createdAt: user.created_at,
updatedAt: user.updated_at
})),
pagination: { page, limit, total, totalPages, hasNext, hasPrev }
});
} catch (error) {
logger.logError(error as Error, 'Users List API');
return NextResponse.json({ error: '內部伺服器錯誤', details: error instanceof Error ? error.message : 'Unknown error' }, { status: 500 });
}
}

View File

@@ -0,0 +1,23 @@
import { NextRequest, NextResponse } from 'next/server';
import { requireAdmin } from '@/lib/auth';
import { db } from '@/lib/database';
export async function GET(request: NextRequest) {
try {
await requireAdmin(request);
const total = await db.queryOne<{ count: number }>('SELECT COUNT(*) as count FROM users');
const admin = await db.queryOne<{ count: number }>("SELECT COUNT(*) as count FROM users WHERE role = 'admin'");
const developer = await db.queryOne<{ count: number }>("SELECT COUNT(*) as count FROM users WHERE role = 'developer'");
const user = await db.queryOne<{ count: number }>("SELECT COUNT(*) as count FROM users WHERE role = 'user'");
const today = await db.queryOne<{ count: number }>("SELECT COUNT(*) as count FROM users WHERE join_date = CURDATE()");
return NextResponse.json({
total: total?.count || 0,
admin: admin?.count || 0,
developer: developer?.count || 0,
user: user?.count || 0,
today: today?.count || 0
});
} catch (error) {
return NextResponse.json({ error: '內部伺服器錯誤', details: error instanceof Error ? error.message : 'Unknown error' }, { status: 500 });
}
}

View File

@@ -135,7 +135,7 @@ export default function CompetitionPage() {
const filteredAwards = getFilteredAwards() const filteredAwards = getFilteredAwards()
return ( return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-blue-50"> <div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-blue-50 flex flex-col">
{/* Header */} {/* Header */}
<header className="bg-white/80 backdrop-blur-sm border-b border-gray-200 sticky top-0 z-50"> <header className="bg-white/80 backdrop-blur-sm border-b border-gray-200 sticky top-0 z-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
@@ -165,9 +165,9 @@ export default function CompetitionPage() {
</div> </div>
</div> </div>
</div> </div>
</header> </header>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <div className="flex-1 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Hero Section */} {/* Hero Section */}
<div className="text-center mb-12"> <div className="text-center mb-12">
<h2 className="text-4xl font-bold text-gray-900 mb-4">AI </h2> <h2 className="text-4xl font-bold text-gray-900 mb-4">AI </h2>

View File

@@ -344,7 +344,7 @@ export default function AIShowcasePlatform() {
const filteredAwards = getFilteredAwards() const filteredAwards = getFilteredAwards()
return ( return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-blue-50"> <div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-blue-50 flex flex-col">
{/* Header */} {/* Header */}
<header className="bg-white/80 backdrop-blur-sm border-b border-gray-200 sticky top-0 z-50"> <header className="bg-white/80 backdrop-blur-sm border-b border-gray-200 sticky top-0 z-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
@@ -401,9 +401,9 @@ export default function AIShowcasePlatform() {
</div> </div>
</div> </div>
</div> </div>
</header> </header>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <div className="flex-1 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{showCompetition ? ( {showCompetition ? (
// Competition Content // Competition Content
<> <>
@@ -999,7 +999,7 @@ export default function AIShowcasePlatform() {
</div> </div>
{/* Footer */} {/* Footer */}
<footer className="bg-white border-t border-gray-200 mt-16"> <footer className="bg-white border-t border-gray-200 mt-auto">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="flex flex-col md:flex-row justify-between items-center"> <div className="flex flex-col md:flex-row justify-between items-center">
<div className="text-sm text-gray-500 mb-4 md:mb-0"> <div className="text-sm text-gray-500 mb-4 md:mb-0">

View File

@@ -78,8 +78,8 @@ export function SystemSettings() {
} }
const handleTestEmail = () => { const handleTestEmail = () => {
// 測試郵件功能 // 測試郵件功能 - 僅用於開發測試
alert("測試郵件已發送!") console.log("測試郵件功能")
} }
const updateSetting = (key: string, value: any) => { const updateSetting = (key: string, value: any) => {

View File

@@ -1,6 +1,6 @@
"use client" "use client"
import { useState } from "react" import { useState, useEffect } from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
@@ -58,6 +58,57 @@ export function UserManagement() {
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [success, setSuccess] = useState("") const [success, setSuccess] = useState("")
const [error, setError] = useState("") const [error, setError] = useState("")
const [stats, setStats] = useState({
total: 0,
admin: 0,
developer: 0,
user: 0,
today: 0
})
// 載入用戶資料
useEffect(() => {
const fetchUsers = async () => {
try {
setIsLoading(true)
// 獲取用戶列表
const usersResponse = await fetch('/api/users', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
})
if (usersResponse.ok) {
const usersData = await usersResponse.json()
setUsers(usersData.users || [])
} else {
console.error('獲取用戶列表失敗')
}
// 獲取統計資料
const statsResponse = await fetch('/api/users/stats', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
})
if (statsResponse.ok) {
const statsData = await statsResponse.json()
setStats(statsData)
} else {
console.error('獲取統計資料失敗')
}
} catch (error) {
console.error('載入用戶資料失敗:', error)
setError('載入用戶資料失敗')
} finally {
setIsLoading(false)
}
}
fetchUsers()
}, [])
// 邀請用戶表單狀態 - 包含電子郵件和預設角色 // 邀請用戶表單狀態 - 包含電子郵件和預設角色
const [inviteEmail, setInviteEmail] = useState("") const [inviteEmail, setInviteEmail] = useState("")
@@ -395,7 +446,7 @@ export function UserManagement() {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-sm text-gray-600"></p> <p className="text-sm text-gray-600"></p>
<p className="text-2xl font-bold">{users.length}</p> <p className="text-2xl font-bold">{stats.total}</p>
</div> </div>
<div className="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center"> <div className="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center">
<Users className="w-4 h-4 text-blue-600" /> <Users className="w-4 h-4 text-blue-600" />
@@ -409,7 +460,7 @@ export function UserManagement() {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-sm text-gray-600"></p> <p className="text-sm text-gray-600"></p>
<p className="text-2xl font-bold">{users.filter((u) => u.status === "active").length}</p> <p className="text-2xl font-bold">{stats.total}</p>
</div> </div>
<div className="w-8 h-8 bg-green-100 rounded-full flex items-center justify-center"> <div className="w-8 h-8 bg-green-100 rounded-full flex items-center justify-center">
<Activity className="w-4 h-4 text-green-600" /> <Activity className="w-4 h-4 text-green-600" />
@@ -423,7 +474,7 @@ export function UserManagement() {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-sm text-gray-600"></p> <p className="text-sm text-gray-600"></p>
<p className="text-2xl font-bold">{users.filter((u) => u.role === "admin").length}</p> <p className="text-2xl font-bold">{stats.admin}</p>
</div> </div>
<div className="w-8 h-8 bg-purple-100 rounded-full flex items-center justify-center"> <div className="w-8 h-8 bg-purple-100 rounded-full flex items-center justify-center">
<Shield className="w-4 h-4 text-purple-600" /> <Shield className="w-4 h-4 text-purple-600" />
@@ -437,7 +488,7 @@ export function UserManagement() {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-sm text-gray-600"></p> <p className="text-sm text-gray-600"></p>
<p className="text-2xl font-bold">{users.filter((u) => u.role === "developer").length}</p> <p className="text-2xl font-bold">{stats.developer}</p>
</div> </div>
<div className="w-8 h-8 bg-green-100 rounded-full flex items-center justify-center"> <div className="w-8 h-8 bg-green-100 rounded-full flex items-center justify-center">
<Code className="w-4 h-4 text-green-600" /> <Code className="w-4 h-4 text-green-600" />
@@ -450,11 +501,11 @@ export function UserManagement() {
<CardContent className="p-4"> <CardContent className="p-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-sm text-gray-600"></p> <p className="text-sm text-gray-600"></p>
<p className="text-2xl font-bold">{users.filter((u) => u.status === "invited").length}</p> <p className="text-2xl font-bold">{stats.user}</p>
</div> </div>
<div className="w-8 h-8 bg-yellow-100 rounded-full flex items-center justify-center"> <div className="w-8 h-8 bg-yellow-100 rounded-full flex items-center justify-center">
<Clock className="w-4 h-4 text-yellow-600" /> <User className="w-4 h-4 text-yellow-600" />
</div> </div>
</div> </div>
</CardContent> </CardContent>
@@ -464,8 +515,8 @@ export function UserManagement() {
<CardContent className="p-4"> <CardContent className="p-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-sm text-gray-600"></p> <p className="text-sm text-gray-600"></p>
<p className="text-2xl font-bold">2</p> <p className="text-2xl font-bold">{stats.today}</p>
</div> </div>
<div className="w-8 h-8 bg-orange-100 rounded-full flex items-center justify-center"> <div className="w-8 h-8 bg-orange-100 rounded-full flex items-center justify-center">
<UserPlus className="w-4 h-4 text-orange-600" /> <UserPlus className="w-4 h-4 text-orange-600" />
@@ -537,21 +588,33 @@ export function UserManagement() {
<CardDescription></CardDescription> <CardDescription></CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<Table> {isLoading ? (
<TableHeader> <div className="flex items-center justify-center py-8">
<TableRow> <Loader2 className="w-6 h-6 animate-spin text-gray-400" />
<TableHead></TableHead> <span className="ml-2 text-gray-600">...</span>
<TableHead></TableHead> </div>
<TableHead></TableHead> ) : filteredUsers.length === 0 ? (
<TableHead></TableHead> <div className="text-center py-8">
<TableHead></TableHead> <Users className="w-12 h-12 text-gray-400 mx-auto mb-4" />
<TableHead></TableHead> <p className="text-gray-600"></p>
<TableHead></TableHead> <p className="text-sm text-gray-500 mt-1">調</p>
<TableHead></TableHead> </div>
</TableRow> ) : (
</TableHeader> <Table>
<TableBody> <TableHeader>
{filteredUsers.map((user) => ( <TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredUsers.map((user) => (
<TableRow key={user.id}> <TableRow key={user.id}>
<TableCell> <TableCell>
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
@@ -642,6 +705,7 @@ export function UserManagement() {
))} ))}
</TableBody> </TableBody>
</Table> </Table>
)}
</CardContent> </CardContent>
</Card> </Card>

View File

@@ -107,11 +107,7 @@ export function LoginDialog({ open, onOpenChange, onSwitchToRegister, onSwitchTo
</Alert> </Alert>
)} )}
<div className="bg-gray-50 p-4 rounded-lg">
<p className="text-sm text-gray-600 mb-2"></p>
<p className="text-sm">Email: zhang@panjit.com</p>
<p className="text-sm">Password: password123</p>
</div>
<Button type="submit" className="w-full" disabled={isLoading}> <Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? "登入中..." : "登入"} {isLoading ? "登入中..." : "登入"}

View File

@@ -86,7 +86,7 @@ export function RegisterDialog({ open, onOpenChange }: RegisterDialogProps) {
<div className="text-center py-6"> <div className="text-center py-6">
<CheckCircle className="w-16 h-16 text-green-500 mx-auto mb-4" /> <CheckCircle className="w-16 h-16 text-green-500 mx-auto mb-4" />
<h3 className="text-lg font-semibold text-green-700 mb-2"></h3> <h3 className="text-lg font-semibold text-green-700 mb-2"></h3>
<p className="text-gray-600"></p> <p className="text-gray-600">使</p>
</div> </div>
) : ( ) : (
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-4">

View File

@@ -24,7 +24,7 @@ interface Message {
quickQuestions?: string[] quickQuestions?: string[]
} }
const DEEPSEEK_API_KEY = process.env.NEXT_PUBLIC_DEEPSEEK_API_KEY || "sk-3640dcff23fe4a069a64f536ac538d75" const DEEPSEEK_API_KEY = process.env.NEXT_PUBLIC_DEEPSEEK_API_KEY || ""
const DEEPSEEK_API_URL = process.env.NEXT_PUBLIC_DEEPSEEK_API_URL || "https://api.deepseek.com/v1/chat/completions" const DEEPSEEK_API_URL = process.env.NEXT_PUBLIC_DEEPSEEK_API_URL || "https://api.deepseek.com/v1/chat/completions"
const systemPrompt = `你是一個競賽管理系統的AI助手專門幫助用戶了解如何使用這個系統。 const systemPrompt = `你是一個競賽管理系統的AI助手專門幫助用戶了解如何使用這個系統。

View File

@@ -131,52 +131,64 @@ export function AuthProvider({ children }: { children: ReactNode }) {
const login = async (email: string, password: string): Promise<boolean> => { const login = async (email: string, password: string): Promise<boolean> => {
setIsLoading(true) setIsLoading(true)
// Simulate API call try {
await new Promise((resolve) => setTimeout(resolve, 1000)) const response = await fetch('/api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email, password }),
})
const foundUser = mockUsers.find((u) => u.email === email) const data = await response.json()
if (foundUser && password === "password123") {
setUser(foundUser) if (response.ok && data.user) {
localStorage.setItem("user", JSON.stringify(foundUser)) setUser(data.user)
localStorage.setItem("user", JSON.stringify(data.user))
localStorage.setItem("token", data.token)
setIsLoading(false)
return true
} else {
console.error('登入失敗:', data.error)
setIsLoading(false)
return false
}
} catch (error) {
console.error('登入錯誤:', error)
setIsLoading(false) setIsLoading(false)
return true return false
} }
setIsLoading(false)
return false
} }
const register = async (userData: RegisterData): Promise<boolean> => { const register = async (userData: RegisterData): Promise<boolean> => {
setIsLoading(true) setIsLoading(true)
// Simulate API call try {
await new Promise((resolve) => setTimeout(resolve, 1000)) const response = await fetch('/api/auth/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(userData),
})
// Check if user already exists const data = await response.json()
const existingUser = mockUsers.find((u) => u.email === userData.email)
if (existingUser) { if (response.ok && data.user) {
setUser(data.user)
localStorage.setItem("user", JSON.stringify(data.user))
setIsLoading(false)
return true
} else {
console.error('註冊失敗:', data.error)
setIsLoading(false)
return false
}
} catch (error) {
console.error('註冊錯誤:', error)
setIsLoading(false) setIsLoading(false)
return false return false
} }
const newUser: User = {
id: Date.now().toString(),
name: userData.name,
email: userData.email,
department: userData.department,
role: "user",
joinDate: new Date().toISOString().split("T")[0],
favoriteApps: [],
recentApps: [],
totalLikes: 0,
totalViews: 0,
}
mockUsers.push(newUser)
setUser(newUser)
localStorage.setItem("user", JSON.stringify(newUser))
setIsLoading(false)
return true
} }
const logout = () => { const logout = () => {

View File

@@ -0,0 +1,77 @@
const mysql = require('mysql2/promise');
// 資料庫配置
const dbConfig = {
host: 'mysql.theaken.com',
port: 33306,
user: 'AI_Platform',
password: 'Aa123456',
database: 'db_AI_Platform',
charset: 'utf8mb4',
timezone: '+08:00'
};
async function testDatabaseConnection() {
let connection;
try {
console.log('🔌 正在連接資料庫...');
console.log(`主機: ${dbConfig.host}:${dbConfig.port}`);
console.log(`資料庫: ${dbConfig.database}`);
console.log(`用戶: ${dbConfig.user}`);
// 建立連接
connection = await mysql.createConnection(dbConfig);
console.log('✅ 資料庫連接成功!');
// 測試查詢
const [rows] = await connection.execute('SELECT VERSION() as version');
console.log(`📊 MySQL版本: ${rows[0].version}`);
// 檢查資料表
const [tables] = await connection.execute(`
SELECT TABLE_NAME, TABLE_ROWS
FROM information_schema.tables
WHERE table_schema = '${dbConfig.database}'
ORDER BY TABLE_NAME
`);
console.log('\n📋 資料表列表:');
console.log('─'.repeat(50));
tables.forEach(table => {
console.log(`${table.TABLE_NAME.padEnd(25)} | ${table.TABLE_ROWS || 0} 筆記錄`);
});
// 檢查用戶數量
const [userCount] = await connection.execute('SELECT COUNT(*) as count FROM users');
console.log(`\n👥 用戶數量: ${userCount[0].count}`);
// 檢查競賽數量
const [compCount] = await connection.execute('SELECT COUNT(*) as count FROM competitions');
console.log(`🏆 競賽數量: ${compCount[0].count}`);
// 檢查評審數量
const [judgeCount] = await connection.execute('SELECT COUNT(*) as count FROM judges');
console.log(`👨‍⚖️ 評審數量: ${judgeCount[0].count}`);
console.log('\n🎉 資料庫連接測試完成!');
} catch (error) {
console.error('❌ 資料庫連接失敗:', error.message);
console.error('請檢查以下項目:');
console.error('1. 資料庫主機是否可達');
console.error('2. 連接埠是否正確');
console.error('3. 用戶名和密碼是否正確');
console.error('4. 資料庫是否存在');
console.error('5. 用戶是否有足夠權限');
} finally {
if (connection) {
await connection.end();
console.log('🔌 資料庫連接已關閉');
}
}
}
// 執行測試
testDatabaseConnection();

511
database_setup.sql Normal file
View File

@@ -0,0 +1,511 @@
-- AI展示平台資料庫建立腳本
-- 資料庫: db_AI_Platform
-- 主機: mysql.theaken.com:33306
-- 用戶: AI_Platform
-- 使用資料庫
USE db_AI_Platform;
-- 1. 用戶表 (users)
CREATE TABLE users (
id VARCHAR(36) PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
avatar VARCHAR(500),
department VARCHAR(100) NOT NULL,
role ENUM('user', 'developer', 'admin') DEFAULT 'user',
join_date DATE NOT NULL,
total_likes INT DEFAULT 0,
total_views INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_email (email),
INDEX idx_department (department),
INDEX idx_role (role)
);
-- 2. 競賽表 (competitions)
CREATE TABLE competitions (
id VARCHAR(36) PRIMARY KEY,
name VARCHAR(200) NOT NULL,
year INT NOT NULL,
month INT NOT NULL,
start_date DATE NOT NULL,
end_date DATE NOT NULL,
status ENUM('upcoming', 'active', 'judging', 'completed') DEFAULT 'upcoming',
description TEXT,
type ENUM('individual', 'team', 'mixed', 'proposal') NOT NULL,
evaluation_focus TEXT,
max_team_size INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_status (status),
INDEX idx_type (type),
INDEX idx_year_month (year, month),
INDEX idx_dates (start_date, end_date)
);
-- 3. 評審表 (judges)
CREATE TABLE judges (
id VARCHAR(36) PRIMARY KEY,
name VARCHAR(100) NOT NULL,
title VARCHAR(100) NOT NULL,
department VARCHAR(100) NOT NULL,
expertise JSON,
avatar VARCHAR(500),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_department (department)
);
-- 4. 團隊表 (teams)
CREATE TABLE teams (
id VARCHAR(36) PRIMARY KEY,
name VARCHAR(200) NOT NULL,
leader_id VARCHAR(36) NOT NULL,
department VARCHAR(100) NOT NULL,
contact_email VARCHAR(255) NOT NULL,
total_likes INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (leader_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_department (department),
INDEX idx_leader (leader_id)
);
-- 5. 團隊成員表 (team_members)
CREATE TABLE team_members (
id VARCHAR(36) PRIMARY KEY,
team_id VARCHAR(36) NOT NULL,
user_id VARCHAR(36) NOT NULL,
role VARCHAR(50) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
UNIQUE KEY unique_team_user (team_id, user_id),
INDEX idx_team (team_id),
INDEX idx_user (user_id)
);
-- 6. 應用表 (apps)
CREATE TABLE apps (
id VARCHAR(36) PRIMARY KEY,
name VARCHAR(200) NOT NULL,
description TEXT,
creator_id VARCHAR(36) NOT NULL,
team_id VARCHAR(36),
likes_count INT DEFAULT 0,
views_count INT DEFAULT 0,
rating DECIMAL(3,2) DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (creator_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE SET NULL,
INDEX idx_creator (creator_id),
INDEX idx_team (team_id),
INDEX idx_rating (rating),
INDEX idx_likes (likes_count)
);
-- 7. 提案表 (proposals) - 新增
CREATE TABLE proposals (
id VARCHAR(36) PRIMARY KEY,
title VARCHAR(200) NOT NULL,
description TEXT,
creator_id VARCHAR(36) NOT NULL,
team_id VARCHAR(36),
status ENUM('draft', 'submitted', 'under_review', 'approved', 'rejected') DEFAULT 'draft',
likes_count INT DEFAULT 0,
views_count INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (creator_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE SET NULL,
INDEX idx_creator (creator_id),
INDEX idx_status (status)
);
-- 8. 評分表 (judge_scores)
CREATE TABLE judge_scores (
id VARCHAR(36) PRIMARY KEY,
judge_id VARCHAR(36) NOT NULL,
app_id VARCHAR(36),
proposal_id VARCHAR(36),
scores JSON NOT NULL,
comments TEXT,
submitted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (judge_id) REFERENCES judges(id) ON DELETE CASCADE,
FOREIGN KEY (app_id) REFERENCES apps(id) ON DELETE CASCADE,
FOREIGN KEY (proposal_id) REFERENCES proposals(id) ON DELETE CASCADE,
UNIQUE KEY unique_judge_app (judge_id, app_id),
UNIQUE KEY unique_judge_proposal (judge_id, proposal_id),
INDEX idx_judge (judge_id),
INDEX idx_app (app_id),
INDEX idx_proposal (proposal_id)
);
-- 9. 獎項表 (awards)
CREATE TABLE awards (
id VARCHAR(36) PRIMARY KEY,
competition_id VARCHAR(36) NOT NULL,
app_id VARCHAR(36),
team_id VARCHAR(36),
proposal_id VARCHAR(36),
award_type ENUM('gold', 'silver', 'bronze', 'popular', 'innovation', 'technical', 'custom') NOT NULL,
award_name VARCHAR(200) NOT NULL,
score DECIMAL(5,2) NOT NULL,
year INT NOT NULL,
month INT NOT NULL,
icon VARCHAR(50),
custom_award_type_id VARCHAR(36),
competition_type ENUM('individual', 'team', 'proposal') NOT NULL,
rank INT DEFAULT 0,
category ENUM('innovation', 'technical', 'practical', 'popular', 'teamwork', 'solution', 'creativity') NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (competition_id) REFERENCES competitions(id) ON DELETE CASCADE,
FOREIGN KEY (app_id) REFERENCES apps(id) ON DELETE SET NULL,
FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE SET NULL,
FOREIGN KEY (proposal_id) REFERENCES proposals(id) ON DELETE SET NULL,
INDEX idx_competition (competition_id),
INDEX idx_award_type (award_type),
INDEX idx_year_month (year, month),
INDEX idx_category (category)
);
-- 10. 聊天會話表 (chat_sessions)
CREATE TABLE chat_sessions (
id VARCHAR(36) PRIMARY KEY,
user_id VARCHAR(36) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_user (user_id),
INDEX idx_created (created_at)
);
-- 11. 聊天訊息表 (chat_messages)
CREATE TABLE chat_messages (
id VARCHAR(36) PRIMARY KEY,
session_id VARCHAR(36) NOT NULL,
text TEXT NOT NULL,
sender ENUM('user', 'bot') NOT NULL,
quick_questions JSON,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (session_id) REFERENCES chat_sessions(id) ON DELETE CASCADE,
INDEX idx_session (session_id),
INDEX idx_created (created_at)
);
-- 12. AI助手配置表 (ai_assistant_configs)
CREATE TABLE ai_assistant_configs (
id VARCHAR(36) PRIMARY KEY,
api_key VARCHAR(255) NOT NULL,
api_url VARCHAR(500) NOT NULL,
model VARCHAR(100) NOT NULL,
max_tokens INT DEFAULT 200,
temperature DECIMAL(3,2) DEFAULT 0.7,
system_prompt TEXT NOT NULL,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_active (is_active)
);
-- 13. 用戶收藏表 (user_favorites) - 新增
CREATE TABLE user_favorites (
id VARCHAR(36) PRIMARY KEY,
user_id VARCHAR(36) NOT NULL,
app_id VARCHAR(36),
proposal_id VARCHAR(36),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (app_id) REFERENCES apps(id) ON DELETE CASCADE,
FOREIGN KEY (proposal_id) REFERENCES proposals(id) ON DELETE CASCADE,
UNIQUE KEY unique_user_app (user_id, app_id),
UNIQUE KEY unique_user_proposal (user_id, proposal_id),
INDEX idx_user (user_id)
);
-- 14. 用戶按讚表 (user_likes) - 新增
CREATE TABLE user_likes (
id VARCHAR(36) PRIMARY KEY,
user_id VARCHAR(36) NOT NULL,
app_id VARCHAR(36),
proposal_id VARCHAR(36),
liked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (app_id) REFERENCES apps(id) ON DELETE CASCADE,
FOREIGN KEY (proposal_id) REFERENCES proposals(id) ON DELETE CASCADE,
UNIQUE KEY unique_user_app_date (user_id, app_id, DATE(liked_at)),
UNIQUE KEY unique_user_proposal_date (user_id, proposal_id, DATE(liked_at)),
INDEX idx_user (user_id),
INDEX idx_app (app_id),
INDEX idx_proposal (proposal_id),
INDEX idx_date (liked_at)
);
-- 15. 競賽參與表 (competition_participants) - 新增
CREATE TABLE competition_participants (
id VARCHAR(36) PRIMARY KEY,
competition_id VARCHAR(36) NOT NULL,
user_id VARCHAR(36),
team_id VARCHAR(36),
app_id VARCHAR(36),
proposal_id VARCHAR(36),
status ENUM('registered', 'submitted', 'approved', 'rejected') DEFAULT 'registered',
registered_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (competition_id) REFERENCES competitions(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE CASCADE,
FOREIGN KEY (app_id) REFERENCES apps(id) ON DELETE CASCADE,
FOREIGN KEY (proposal_id) REFERENCES proposals(id) ON DELETE CASCADE,
INDEX idx_competition (competition_id),
INDEX idx_user (user_id),
INDEX idx_team (team_id),
INDEX idx_status (status)
);
-- 16. 競賽評審分配表 (competition_judges) - 新增
CREATE TABLE competition_judges (
id VARCHAR(36) PRIMARY KEY,
competition_id VARCHAR(36) NOT NULL,
judge_id VARCHAR(36) NOT NULL,
assigned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (competition_id) REFERENCES competitions(id) ON DELETE CASCADE,
FOREIGN KEY (judge_id) REFERENCES judges(id) ON DELETE CASCADE,
UNIQUE KEY unique_competition_judge (competition_id, judge_id),
INDEX idx_competition (competition_id),
INDEX idx_judge (judge_id)
);
-- 17. 系統設定表 (system_settings) - 新增
CREATE TABLE system_settings (
id VARCHAR(36) PRIMARY KEY,
setting_key VARCHAR(100) UNIQUE NOT NULL,
setting_value TEXT,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_key (setting_key)
);
-- 18. 活動日誌表 (activity_logs) - 新增
CREATE TABLE activity_logs (
id VARCHAR(36) PRIMARY KEY,
user_id VARCHAR(36),
action VARCHAR(100) NOT NULL,
target_type ENUM('user', 'competition', 'app', 'proposal', 'team', 'award') NOT NULL,
target_id VARCHAR(36),
details JSON,
ip_address VARCHAR(45),
user_agent TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL,
INDEX idx_user (user_id),
INDEX idx_action (action),
INDEX idx_target (target_type, target_id),
INDEX idx_created (created_at)
);
-- 插入初始數據
-- 1. 插入預設管理員用戶 (密碼: admin123)
INSERT INTO users (id, name, email, password_hash, department, role, join_date) VALUES
('admin-001', '系統管理員', 'admin@theaken.com', '$2b$10$rQZ8K9mN2pL1vX3yU7wE4tA6sB8cD1eF2gH3iJ4kL5mN6oP7qR8sT9uV0wX1yZ2a', '資訊部', 'admin', '2025-01-01');
-- 2. 插入預設評審
INSERT INTO judges (id, name, title, department, expertise) VALUES
('judge-001', '張教授', '資深技術顧問', '研發部', '["AI", "機器學習", "深度學習"]'),
('judge-002', '李經理', '產品經理', '產品部', '["產品設計", "用戶體驗", "市場分析"]'),
('judge-003', '王工程師', '資深工程師', '技術部', '["軟體開發", "系統架構", "雲端技術"]');
-- 3. 插入預設競賽
INSERT INTO competitions (id, name, year, month, start_date, end_date, status, description, type, evaluation_focus, max_team_size) VALUES
('comp-2025-01', '2025年AI創新競賽', 2025, 1, '2025-01-15', '2025-03-15', 'upcoming', '年度AI技術創新競賽鼓勵員工開發創新AI應用', 'mixed', '創新性、技術實現、實用價值', 5),
('comp-2025-02', '2025年提案競賽', 2025, 2, '2025-02-01', '2025-04-01', 'upcoming', 'AI解決方案提案競賽', 'proposal', '解決方案可行性、創新程度、商業價值', NULL);
-- 4. 插入AI助手配置
INSERT INTO ai_assistant_configs (id, api_key, api_url, model, max_tokens, temperature, system_prompt, is_active) VALUES
('ai-config-001', 'your_deepseek_api_key_here', 'https://api.deepseek.com/v1/chat/completions', 'deepseek-chat', 200, 0.7, '你是一個AI展示平台的智能助手專門協助用戶使用平台功能。請用友善、專業的態度回答問題。', TRUE);
-- 5. 插入系統設定
INSERT INTO system_settings (setting_key, setting_value, description) VALUES
('daily_like_limit', '5', '用戶每日按讚限制'),
('max_team_size', '5', '最大團隊人數'),
('competition_registration_deadline', '7', '競賽報名截止天數'),
('judge_score_weight_innovation', '25', '創新性評分權重'),
('judge_score_weight_technical', '25', '技術性評分權重'),
('judge_score_weight_usability', '20', '實用性評分權重'),
('judge_score_weight_presentation', '15', '展示效果評分權重'),
('judge_score_weight_impact', '15', '影響力評分權重');
-- 建立視圖 (Views)
-- 1. 用戶統計視圖
CREATE VIEW user_statistics AS
SELECT
u.id,
u.name,
u.email,
u.department,
u.role,
COUNT(DISTINCT a.id) as total_apps,
COUNT(DISTINCT t.id) as total_teams,
COUNT(DISTINCT f.app_id) as total_favorites,
COUNT(DISTINCT l.app_id) as total_likes,
u.total_views
FROM users u
LEFT JOIN apps a ON u.id = a.creator_id
LEFT JOIN team_members tm ON u.id = tm.user_id
LEFT JOIN teams t ON tm.team_id = t.id
LEFT JOIN user_favorites f ON u.id = f.user_id
LEFT JOIN user_likes l ON u.id = l.user_id
GROUP BY u.id;
-- 2. 競賽統計視圖
CREATE VIEW competition_statistics AS
SELECT
c.id,
c.name,
c.year,
c.month,
c.status,
c.type,
COUNT(DISTINCT cp.user_id) as participant_count,
COUNT(DISTINCT cp.team_id) as team_count,
COUNT(DISTINCT cp.app_id) as app_count,
COUNT(DISTINCT cp.proposal_id) as proposal_count,
COUNT(DISTINCT cj.judge_id) as judge_count
FROM competitions c
LEFT JOIN competition_participants cp ON c.id = cp.competition_id
LEFT JOIN competition_judges cj ON c.id = cj.competition_id
GROUP BY c.id;
-- 3. 應用排行榜視圖
CREATE VIEW app_rankings AS
SELECT
a.id,
a.name,
a.description,
u.name as creator_name,
t.name as team_name,
a.likes_count,
a.views_count,
a.rating,
ROW_NUMBER() OVER (ORDER BY a.likes_count DESC) as popularity_rank,
ROW_NUMBER() OVER (ORDER BY a.rating DESC) as rating_rank,
a.created_at
FROM apps a
LEFT JOIN users u ON a.creator_id = u.id
LEFT JOIN teams t ON a.team_id = t.id;
-- 建立觸發器 (Triggers)
-- 1. 更新用戶總按讚數
DELIMITER //
CREATE TRIGGER update_user_total_likes
AFTER INSERT ON user_likes
FOR EACH ROW
BEGIN
UPDATE users
SET total_likes = total_likes + 1
WHERE id = NEW.user_id;
END//
CREATE TRIGGER update_app_likes_count
AFTER INSERT ON user_likes
FOR EACH ROW
BEGIN
IF NEW.app_id IS NOT NULL THEN
UPDATE apps
SET likes_count = likes_count + 1
WHERE id = NEW.app_id;
END IF;
END//
DELIMITER ;
-- 2. 更新用戶總瀏覽數
DELIMITER //
CREATE TRIGGER update_user_total_views
AFTER UPDATE ON apps
FOR EACH ROW
BEGIN
IF NEW.views_count != OLD.views_count THEN
UPDATE users
SET total_views = total_views + (NEW.views_count - OLD.views_count)
WHERE id = NEW.creator_id;
END IF;
END//
DELIMITER ;
-- 建立存儲過程 (Stored Procedures)
-- 1. 獲取用戶權限
DELIMITER //
CREATE PROCEDURE GetUserPermissions(IN user_email VARCHAR(255))
BEGIN
SELECT
id,
name,
email,
role,
department,
CASE
WHEN role = 'admin' THEN TRUE
ELSE FALSE
END as is_admin,
CASE
WHEN role IN ('developer', 'admin') THEN TRUE
ELSE FALSE
END as can_submit_app
FROM users
WHERE email = user_email;
END//
DELIMITER ;
-- 2. 獲取競賽統計
DELIMITER //
CREATE PROCEDURE GetCompetitionStats(IN comp_id VARCHAR(36))
BEGIN
SELECT
c.name,
c.status,
c.type,
COUNT(DISTINCT cp.user_id) as participant_count,
COUNT(DISTINCT cp.team_id) as team_count,
COUNT(DISTINCT cp.app_id) as app_count,
COUNT(DISTINCT cp.proposal_id) as proposal_count,
COUNT(DISTINCT cj.judge_id) as judge_count
FROM competitions c
LEFT JOIN competition_participants cp ON c.id = cp.competition_id
LEFT JOIN competition_judges cj ON c.id = cj.competition_id
WHERE c.id = comp_id
GROUP BY c.id;
END//
DELIMITER ;
-- 3. 計算獎項排名
DELIMITER //
CREATE PROCEDURE CalculateAwardRankings(IN comp_id VARCHAR(36))
BEGIN
SELECT
a.id,
a.award_name,
a.score,
a.rank,
a.category,
CASE
WHEN a.app_id IS NOT NULL THEN (SELECT name FROM apps WHERE id = a.app_id)
WHEN a.team_id IS NOT NULL THEN (SELECT name FROM teams WHERE id = a.team_id)
WHEN a.proposal_id IS NOT NULL THEN (SELECT title FROM proposals WHERE id = a.proposal_id)
END as winner_name
FROM awards a
WHERE a.competition_id = comp_id
ORDER BY a.rank ASC, a.score DESC;
END//
DELIMITER ;
-- 顯示建立結果
SELECT 'Database setup completed successfully!' as status;
SELECT COUNT(*) as total_tables FROM information_schema.tables WHERE table_schema = 'db_AI_Platform';

344
database_setup_simple.sql Normal file
View File

@@ -0,0 +1,344 @@
-- AI展示平台資料庫建立腳本 (簡化版)
-- 資料庫: db_AI_Platform
-- 主機: mysql.theaken.com:33306
-- 用戶: AI_Platform
-- 使用資料庫
USE db_AI_Platform;
-- 1. 用戶表 (users)
CREATE TABLE IF NOT EXISTS users (
id VARCHAR(36) PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
avatar VARCHAR(500),
department VARCHAR(100) NOT NULL,
role ENUM('user', 'developer', 'admin') DEFAULT 'user',
join_date DATE NOT NULL,
total_likes INT DEFAULT 0,
total_views INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_email (email),
INDEX idx_department (department),
INDEX idx_role (role)
);
-- 2. 競賽表 (competitions)
CREATE TABLE IF NOT EXISTS competitions (
id VARCHAR(36) PRIMARY KEY,
name VARCHAR(200) NOT NULL,
year INT NOT NULL,
month INT NOT NULL,
start_date DATE NOT NULL,
end_date DATE NOT NULL,
status ENUM('upcoming', 'active', 'judging', 'completed') DEFAULT 'upcoming',
description TEXT,
type ENUM('individual', 'team', 'mixed', 'proposal') NOT NULL,
evaluation_focus TEXT,
max_team_size INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_status (status),
INDEX idx_type (type),
INDEX idx_year_month (year, month),
INDEX idx_dates (start_date, end_date)
);
-- 3. 評審表 (judges)
CREATE TABLE IF NOT EXISTS judges (
id VARCHAR(36) PRIMARY KEY,
name VARCHAR(100) NOT NULL,
title VARCHAR(100) NOT NULL,
department VARCHAR(100) NOT NULL,
expertise JSON,
avatar VARCHAR(500),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_department (department)
);
-- 4. 團隊表 (teams)
CREATE TABLE IF NOT EXISTS teams (
id VARCHAR(36) PRIMARY KEY,
name VARCHAR(200) NOT NULL,
leader_id VARCHAR(36) NOT NULL,
department VARCHAR(100) NOT NULL,
contact_email VARCHAR(255) NOT NULL,
total_likes INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (leader_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_department (department),
INDEX idx_leader (leader_id)
);
-- 5. 團隊成員表 (team_members)
CREATE TABLE IF NOT EXISTS team_members (
id VARCHAR(36) PRIMARY KEY,
team_id VARCHAR(36) NOT NULL,
user_id VARCHAR(36) NOT NULL,
role VARCHAR(50) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
UNIQUE KEY unique_team_user (team_id, user_id),
INDEX idx_team (team_id),
INDEX idx_user (user_id)
);
-- 6. 應用表 (apps)
CREATE TABLE IF NOT EXISTS apps (
id VARCHAR(36) PRIMARY KEY,
name VARCHAR(200) NOT NULL,
description TEXT,
creator_id VARCHAR(36) NOT NULL,
team_id VARCHAR(36),
likes_count INT DEFAULT 0,
views_count INT DEFAULT 0,
rating DECIMAL(3,2) DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (creator_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE SET NULL,
INDEX idx_creator (creator_id),
INDEX idx_team (team_id),
INDEX idx_rating (rating),
INDEX idx_likes (likes_count)
);
-- 7. 提案表 (proposals)
CREATE TABLE IF NOT EXISTS proposals (
id VARCHAR(36) PRIMARY KEY,
title VARCHAR(200) NOT NULL,
description TEXT,
creator_id VARCHAR(36) NOT NULL,
team_id VARCHAR(36),
status ENUM('draft', 'submitted', 'under_review', 'approved', 'rejected') DEFAULT 'draft',
likes_count INT DEFAULT 0,
views_count INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (creator_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE SET NULL,
INDEX idx_creator (creator_id),
INDEX idx_status (status)
);
-- 8. 評分表 (judge_scores)
CREATE TABLE IF NOT EXISTS judge_scores (
id VARCHAR(36) PRIMARY KEY,
judge_id VARCHAR(36) NOT NULL,
app_id VARCHAR(36),
proposal_id VARCHAR(36),
scores JSON NOT NULL,
comments TEXT,
submitted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (judge_id) REFERENCES judges(id) ON DELETE CASCADE,
FOREIGN KEY (app_id) REFERENCES apps(id) ON DELETE CASCADE,
FOREIGN KEY (proposal_id) REFERENCES proposals(id) ON DELETE CASCADE,
UNIQUE KEY unique_judge_app (judge_id, app_id),
UNIQUE KEY unique_judge_proposal (judge_id, proposal_id),
INDEX idx_judge (judge_id),
INDEX idx_app (app_id),
INDEX idx_proposal (proposal_id)
);
-- 9. 獎項表 (awards)
CREATE TABLE IF NOT EXISTS awards (
id VARCHAR(36) PRIMARY KEY,
competition_id VARCHAR(36) NOT NULL,
app_id VARCHAR(36),
team_id VARCHAR(36),
proposal_id VARCHAR(36),
award_type ENUM('gold', 'silver', 'bronze', 'popular', 'innovation', 'technical', 'custom') NOT NULL,
award_name VARCHAR(200) NOT NULL,
score DECIMAL(5,2) NOT NULL,
year INT NOT NULL,
month INT NOT NULL,
icon VARCHAR(50),
custom_award_type_id VARCHAR(36),
competition_type ENUM('individual', 'team', 'proposal') NOT NULL,
rank INT DEFAULT 0,
category ENUM('innovation', 'technical', 'practical', 'popular', 'teamwork', 'solution', 'creativity') NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (competition_id) REFERENCES competitions(id) ON DELETE CASCADE,
FOREIGN KEY (app_id) REFERENCES apps(id) ON DELETE SET NULL,
FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE SET NULL,
FOREIGN KEY (proposal_id) REFERENCES proposals(id) ON DELETE SET NULL,
INDEX idx_competition (competition_id),
INDEX idx_award_type (award_type),
INDEX idx_year_month (year, month),
INDEX idx_category (category)
);
-- 10. 聊天會話表 (chat_sessions)
CREATE TABLE IF NOT EXISTS chat_sessions (
id VARCHAR(36) PRIMARY KEY,
user_id VARCHAR(36) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_user (user_id),
INDEX idx_created (created_at)
);
-- 11. 聊天訊息表 (chat_messages)
CREATE TABLE IF NOT EXISTS chat_messages (
id VARCHAR(36) PRIMARY KEY,
session_id VARCHAR(36) NOT NULL,
text TEXT NOT NULL,
sender ENUM('user', 'bot') NOT NULL,
quick_questions JSON,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (session_id) REFERENCES chat_sessions(id) ON DELETE CASCADE,
INDEX idx_session (session_id),
INDEX idx_created (created_at)
);
-- 12. AI助手配置表 (ai_assistant_configs)
CREATE TABLE IF NOT EXISTS ai_assistant_configs (
id VARCHAR(36) PRIMARY KEY,
api_key VARCHAR(255) NOT NULL,
api_url VARCHAR(500) NOT NULL,
model VARCHAR(100) NOT NULL,
max_tokens INT DEFAULT 200,
temperature DECIMAL(3,2) DEFAULT 0.7,
system_prompt TEXT NOT NULL,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_active (is_active)
);
-- 13. 用戶收藏表 (user_favorites)
CREATE TABLE IF NOT EXISTS user_favorites (
id VARCHAR(36) PRIMARY KEY,
user_id VARCHAR(36) NOT NULL,
app_id VARCHAR(36),
proposal_id VARCHAR(36),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (app_id) REFERENCES apps(id) ON DELETE CASCADE,
FOREIGN KEY (proposal_id) REFERENCES proposals(id) ON DELETE CASCADE,
UNIQUE KEY unique_user_app (user_id, app_id),
UNIQUE KEY unique_user_proposal (user_id, proposal_id),
INDEX idx_user (user_id)
);
-- 14. 用戶按讚表 (user_likes)
CREATE TABLE IF NOT EXISTS user_likes (
id VARCHAR(36) PRIMARY KEY,
user_id VARCHAR(36) NOT NULL,
app_id VARCHAR(36),
proposal_id VARCHAR(36),
liked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (app_id) REFERENCES apps(id) ON DELETE CASCADE,
FOREIGN KEY (proposal_id) REFERENCES proposals(id) ON DELETE CASCADE,
UNIQUE KEY unique_user_app_date (user_id, app_id, DATE(liked_at)),
UNIQUE KEY unique_user_proposal_date (user_id, proposal_id, DATE(liked_at)),
INDEX idx_user (user_id),
INDEX idx_app (app_id),
INDEX idx_proposal (proposal_id),
INDEX idx_date (liked_at)
);
-- 15. 競賽參與表 (competition_participants)
CREATE TABLE IF NOT EXISTS competition_participants (
id VARCHAR(36) PRIMARY KEY,
competition_id VARCHAR(36) NOT NULL,
user_id VARCHAR(36),
team_id VARCHAR(36),
app_id VARCHAR(36),
proposal_id VARCHAR(36),
status ENUM('registered', 'submitted', 'approved', 'rejected') DEFAULT 'registered',
registered_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (competition_id) REFERENCES competitions(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE CASCADE,
FOREIGN KEY (app_id) REFERENCES apps(id) ON DELETE CASCADE,
FOREIGN KEY (proposal_id) REFERENCES proposals(id) ON DELETE CASCADE,
INDEX idx_competition (competition_id),
INDEX idx_user (user_id),
INDEX idx_team (team_id),
INDEX idx_status (status)
);
-- 16. 競賽評審分配表 (competition_judges)
CREATE TABLE IF NOT EXISTS competition_judges (
id VARCHAR(36) PRIMARY KEY,
competition_id VARCHAR(36) NOT NULL,
judge_id VARCHAR(36) NOT NULL,
assigned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (competition_id) REFERENCES competitions(id) ON DELETE CASCADE,
FOREIGN KEY (judge_id) REFERENCES judges(id) ON DELETE CASCADE,
UNIQUE KEY unique_competition_judge (competition_id, judge_id),
INDEX idx_competition (competition_id),
INDEX idx_judge (judge_id)
);
-- 17. 系統設定表 (system_settings)
CREATE TABLE IF NOT EXISTS system_settings (
id VARCHAR(36) PRIMARY KEY,
setting_key VARCHAR(100) UNIQUE NOT NULL,
setting_value TEXT,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_key (setting_key)
);
-- 18. 活動日誌表 (activity_logs)
CREATE TABLE IF NOT EXISTS activity_logs (
id VARCHAR(36) PRIMARY KEY,
user_id VARCHAR(36),
action VARCHAR(100) NOT NULL,
target_type ENUM('user', 'competition', 'app', 'proposal', 'team', 'award') NOT NULL,
target_id VARCHAR(36),
details JSON,
ip_address VARCHAR(45),
user_agent TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL,
INDEX idx_user (user_id),
INDEX idx_action (action),
INDEX idx_target (target_type, target_id),
INDEX idx_created (created_at)
);
-- 插入初始數據
-- 1. 插入預設管理員用戶 (密碼: admin123)
INSERT IGNORE INTO users (id, name, email, password_hash, department, role, join_date) VALUES
('admin-001', '系統管理員', 'admin@theaken.com', '$2b$10$rQZ8K9mN2pL1vX3yU7wE4tA6sB8cD1eF2gH3iJ4kL5mN6oP7qR8sT9uV0wX1yZ2a', '資訊部', 'admin', '2025-01-01');
-- 2. 插入預設評審
INSERT IGNORE INTO judges (id, name, title, department, expertise) VALUES
('judge-001', '張教授', '資深技術顧問', '研發部', '["AI", "機器學習", "深度學習"]'),
('judge-002', '李經理', '產品經理', '產品部', '["產品設計", "用戶體驗", "市場分析"]'),
('judge-003', '王工程師', '資深工程師', '技術部', '["軟體開發", "系統架構", "雲端技術"]');
-- 3. 插入預設競賽
INSERT IGNORE INTO competitions (id, name, year, month, start_date, end_date, status, description, type, evaluation_focus, max_team_size) VALUES
('comp-2025-01', '2025年AI創新競賽', 2025, 1, '2025-01-15', '2025-03-15', 'upcoming', '年度AI技術創新競賽鼓勵員工開發創新AI應用', 'mixed', '創新性、技術實現、實用價值', 5),
('comp-2025-02', '2025年提案競賽', 2025, 2, '2025-02-01', '2025-04-01', 'upcoming', 'AI解決方案提案競賽', 'proposal', '解決方案可行性、創新程度、商業價值', NULL);
-- 4. 插入AI助手配置
INSERT IGNORE INTO ai_assistant_configs (id, api_key, api_url, model, max_tokens, temperature, system_prompt, is_active) VALUES
('ai-config-001', 'your_deepseek_api_key_here', 'https://api.deepseek.com/v1/chat/completions', 'deepseek-chat', 200, 0.7, '你是一個AI展示平台的智能助手專門協助用戶使用平台功能。請用友善、專業的態度回答問題。', TRUE);
-- 5. 插入系統設定
INSERT IGNORE INTO system_settings (setting_key, setting_value, description) VALUES
('daily_like_limit', '5', '用戶每日按讚限制'),
('max_team_size', '5', '最大團隊人數'),
('competition_registration_deadline', '7', '競賽報名截止天數'),
('judge_score_weight_innovation', '25', '創新性評分權重'),
('judge_score_weight_technical', '25', '技術性評分權重'),
('judge_score_weight_usability', '20', '實用性評分權重'),
('judge_score_weight_presentation', '15', '展示效果評分權重'),
('judge_score_weight_impact', '15', '影響力評分權重');
-- 顯示建立結果
SELECT 'Database setup completed successfully!' as status;

36
env.example Normal file
View File

@@ -0,0 +1,36 @@
# 資料庫配置
DB_HOST=mysql.theaken.com
DB_PORT=33306
DB_USER=AI_Platform
DB_PASSWORD=Aa123456
DB_NAME=db_AI_Platform
# JWT 配置
JWT_SECRET=your_jwt_secret_key_here_make_it_long_and_random
JWT_EXPIRES_IN=7d
# AI助手配置
NEXT_PUBLIC_DEEPSEEK_API_KEY=your_deepseek_api_key_here
NEXT_PUBLIC_DEEPSEEK_API_URL=https://api.deepseek.com/v1/chat/completions
# 應用配置
NEXT_PUBLIC_APP_URL=http://localhost:3000
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=your_nextauth_secret_here
# 郵件配置 (可選)
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=your_email@gmail.com
SMTP_PASS=your_email_password
# 文件上傳配置 (可選)
UPLOAD_DIR=./uploads
MAX_FILE_SIZE=10485760
# 快取配置 (可選)
REDIS_URL=redis://localhost:6379
# 日誌配置
LOG_LEVEL=info
LOG_FILE=./logs/app.log

209
lib/auth.ts Normal file
View File

@@ -0,0 +1,209 @@
import jwt from 'jsonwebtoken';
import { NextRequest } from 'next/server';
import { db } from './database';
import bcrypt from 'bcrypt';
// JWT 配置
const JWT_SECRET = process.env.JWT_SECRET || 'ai_platform_jwt_secret_key_2024';
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '7d';
// 用戶角色類型
export type UserRole = 'user' | 'developer' | 'admin';
// 用戶介面
export interface User {
id: string;
name: string;
email: string;
avatar?: string;
department: string;
role: UserRole;
joinDate: string;
totalLikes: number;
totalViews: number;
createdAt: string;
updatedAt: string;
}
// JWT Payload 介面
export interface JWTPayload {
userId: string;
email: string;
role: UserRole;
iat: number;
exp: number;
}
// 生成 JWT Token
export function generateToken(user: { id: string; email: string; role: UserRole }): string {
const payload: Omit<JWTPayload, 'iat' | 'exp'> = {
userId: user.id,
email: user.email,
role: user.role
};
return jwt.sign(payload, JWT_SECRET, {
expiresIn: JWT_EXPIRES_IN
});
}
// 驗證 JWT Token
export function verifyToken(token: string): JWTPayload | null {
try {
return jwt.verify(token, JWT_SECRET) as JWTPayload;
} catch (error) {
console.error('Token verification failed:', error);
return null;
}
}
// 從請求中提取 Token
export function extractToken(request: NextRequest): string | null {
const authHeader = request.headers.get('authorization');
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return null;
}
return authHeader.substring(7);
}
// 驗證用戶權限
export function hasPermission(userRole: UserRole, requiredRole: UserRole): boolean {
const roleHierarchy = {
user: 1,
developer: 2,
admin: 3
};
return roleHierarchy[userRole] >= roleHierarchy[requiredRole];
}
// 用戶認證中間件
export async function authenticateUser(request: NextRequest): Promise<User | null> {
try {
const token = extractToken(request);
if (!token) {
return null;
}
const payload = verifyToken(token);
if (!payload) {
return null;
}
// 從資料庫獲取最新用戶資料
const user = await db.queryOne<User>(
'SELECT * FROM users WHERE id = ? AND email = ?',
[payload.userId, payload.email]
);
return user;
} catch (error) {
console.error('Authentication error:', error);
return null;
}
}
// 檢查管理員權限
export async function requireAdmin(request: NextRequest): Promise<User> {
const user = await authenticateUser(request);
if (!user) {
throw new Error('Authentication required');
}
if (user.role !== 'admin') {
throw new Error('Admin permission required');
}
return user;
}
// 檢查開發者或管理員權限
export async function requireDeveloperOrAdmin(request: NextRequest): Promise<User> {
const user = await authenticateUser(request);
if (!user) {
throw new Error('Authentication required');
}
if (user.role !== 'developer' && user.role !== 'admin') {
throw new Error('Developer or admin permission required');
}
return user;
}
// 密碼驗證
export async function validatePassword(password: string): Promise<{ isValid: boolean; errors: string[] }> {
const errors: string[] = [];
if (password.length < 8) {
errors.push('密碼長度至少需要8個字符');
}
if (!/[A-Z]/.test(password)) {
errors.push('密碼需要包含至少一個大寫字母');
}
if (!/[a-z]/.test(password)) {
errors.push('密碼需要包含至少一個小寫字母');
}
if (!/\d/.test(password)) {
errors.push('密碼需要包含至少一個數字');
}
return {
isValid: errors.length === 0,
errors
};
}
// 加密密碼
export async function hashPassword(password: string): Promise<string> {
const saltRounds = 12;
return bcrypt.hash(password, saltRounds);
}
// 驗證密碼
export async function comparePassword(password: string, hash: string): Promise<boolean> {
return bcrypt.compare(password, hash);
}
// 生成隨機密碼
export function generateRandomPassword(length: number = 12): string {
const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*';
let password = '';
for (let i = 0; i < length; i++) {
password += charset.charAt(Math.floor(Math.random() * charset.length));
}
return password;
}
// 用戶資料驗證
export function validateUserData(data: any): { isValid: boolean; errors: string[] } {
const errors: string[] = [];
if (!data.name || data.name.trim().length < 2) {
errors.push('姓名至少需要2個字符');
}
if (!data.email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
errors.push('請提供有效的電子郵件地址');
}
if (!data.department || data.department.trim().length < 2) {
errors.push('部門名稱至少需要2個字符');
}
if (data.role && !['user', 'developer', 'admin'].includes(data.role)) {
errors.push('無效的用戶角色');
}
return {
isValid: errors.length === 0,
errors
};
}

213
lib/database.ts Normal file
View File

@@ -0,0 +1,213 @@
import mysql from 'mysql2/promise';
// 資料庫配置
const dbConfig = {
host: process.env.DB_HOST || 'mysql.theaken.com',
port: parseInt(process.env.DB_PORT || '33306'),
user: process.env.DB_USER || 'AI_Platform',
password: process.env.DB_PASSWORD || 'Aa123456',
database: process.env.DB_NAME || 'db_AI_Platform',
charset: 'utf8mb4',
timezone: '+08:00',
// 連接池配置
connectionLimit: 10,
acquireTimeout: 60000,
timeout: 60000,
reconnect: true
};
// 創建連接池
let pool: mysql.Pool | null = null;
export class Database {
private static instance: Database;
private constructor() {}
public static getInstance(): Database {
if (!Database.instance) {
Database.instance = new Database();
}
return Database.instance;
}
// 獲取連接池
public async getPool(): Promise<mysql.Pool> {
if (!pool) {
pool = mysql.createPool(dbConfig);
// 測試連接
try {
const connection = await pool.getConnection();
console.log('✅ 資料庫連接池建立成功');
connection.release();
} catch (error) {
console.error('❌ 資料庫連接池建立失敗:', error);
throw error;
}
}
return pool;
}
// 執行查詢
public async query<T = any>(sql: string, params?: any[]): Promise<T[]> {
const pool = await this.getPool();
try {
const [rows] = await pool.execute(sql, params);
return rows as T[];
} catch (error) {
console.error('查詢執行失敗:', error);
throw error;
}
}
// 執行單一查詢 (返回第一筆結果)
public async queryOne<T = any>(sql: string, params?: any[]): Promise<T | null> {
const results = await this.query<T>(sql, params);
return results.length > 0 ? results[0] : null;
}
// 執行插入
public async insert(table: string, data: Record<string, any>): Promise<number> {
const columns = Object.keys(data);
const values = Object.values(data);
const placeholders = columns.map(() => '?').join(', ');
const sql = `INSERT INTO ${table} (${columns.join(', ')}) VALUES (${placeholders})`;
const pool = await this.getPool();
try {
const [result] = await pool.execute(sql, values);
return (result as any).insertId;
} catch (error) {
console.error('插入執行失敗:', error);
throw error;
}
}
// 執行更新
public async update(table: string, data: Record<string, any>, where: Record<string, any>): Promise<number> {
const setColumns = Object.keys(data).map(col => `${col} = ?`).join(', ');
const whereColumns = Object.keys(where).map(col => `${col} = ?`).join(' AND ');
const sql = `UPDATE ${table} SET ${setColumns} WHERE ${whereColumns}`;
const values = [...Object.values(data), ...Object.values(where)];
const pool = await this.getPool();
try {
const [result] = await pool.execute(sql, values);
return (result as any).affectedRows;
} catch (error) {
console.error('更新執行失敗:', error);
throw error;
}
}
// 執行刪除
public async delete(table: string, where: Record<string, any>): Promise<number> {
const whereColumns = Object.keys(where).map(col => `${col} = ?`).join(' AND ');
const sql = `DELETE FROM ${table} WHERE ${whereColumns}`;
const values = Object.values(where);
const pool = await this.getPool();
try {
const [result] = await pool.execute(sql, values);
return (result as any).affectedRows;
} catch (error) {
console.error('刪除執行失敗:', error);
throw error;
}
}
// 開始事務
public async beginTransaction(): Promise<mysql.PoolConnection> {
const pool = await this.getPool();
const connection = await pool.getConnection();
await connection.beginTransaction();
return connection;
}
// 提交事務
public async commitTransaction(connection: mysql.PoolConnection): Promise<void> {
await connection.commit();
connection.release();
}
// 回滾事務
public async rollbackTransaction(connection: mysql.PoolConnection): Promise<void> {
await connection.rollback();
connection.release();
}
// 關閉連接池
public async close(): Promise<void> {
if (pool) {
await pool.end();
pool = null;
console.log('🔌 資料庫連接池已關閉');
}
}
// 健康檢查
public async healthCheck(): Promise<boolean> {
try {
const result = await this.queryOne('SELECT 1 as health');
return result?.health === 1;
} catch (error) {
console.error('資料庫健康檢查失敗:', error);
return false;
}
}
// 獲取資料庫統計
public async getDatabaseStats(): Promise<{
tables: number;
users: number;
competitions: number;
apps: number;
judges: number;
}> {
try {
const tablesResult = await this.queryOne(`
SELECT COUNT(*) as count
FROM information_schema.tables
WHERE table_schema = ?
`, [dbConfig.database]);
const usersResult = await this.queryOne('SELECT COUNT(*) as count FROM users');
const competitionsResult = await this.queryOne('SELECT COUNT(*) as count FROM competitions');
const appsResult = await this.queryOne('SELECT COUNT(*) as count FROM apps');
const judgesResult = await this.queryOne('SELECT COUNT(*) as count FROM judges');
return {
tables: tablesResult?.count || 0,
users: usersResult?.count || 0,
competitions: competitionsResult?.count || 0,
apps: appsResult?.count || 0,
judges: judgesResult?.count || 0
};
} catch (error) {
console.error('獲取資料庫統計失敗:', error);
throw error;
}
}
}
// 導出單例實例
export const db = Database.getInstance();
import bcrypt from 'bcrypt';
// 工具函數
export const generateId = (): string => {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
};
export const hashPassword = async (password: string): Promise<string> => {
const saltRounds = 12;
return bcrypt.hash(password, saltRounds);
};
export const comparePassword = async (password: string, hash: string): Promise<boolean> => {
return bcrypt.compare(password, hash);
};

163
lib/logger.ts Normal file
View File

@@ -0,0 +1,163 @@
import fs from 'fs';
import path from 'path';
// 日誌級別
export enum LogLevel {
ERROR = 0,
WARN = 1,
INFO = 2,
DEBUG = 3
}
// 日誌配置
const LOG_LEVEL = (process.env.LOG_LEVEL as LogLevel) || LogLevel.INFO;
const LOG_FILE = process.env.LOG_FILE || './logs/app.log';
// 確保日誌目錄存在
const logDir = path.dirname(LOG_FILE);
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true });
}
// 日誌顏色
const colors = {
reset: '\x1b[0m',
red: '\x1b[31m',
yellow: '\x1b[33m',
green: '\x1b[32m',
blue: '\x1b[34m',
magenta: '\x1b[35m',
cyan: '\x1b[36m'
};
// 日誌級別名稱
const levelNames = {
[LogLevel.ERROR]: 'ERROR',
[LogLevel.WARN]: 'WARN',
[LogLevel.INFO]: 'INFO',
[LogLevel.DEBUG]: 'DEBUG'
};
// 日誌級別顏色
const levelColors = {
[LogLevel.ERROR]: colors.red,
[LogLevel.WARN]: colors.yellow,
[LogLevel.INFO]: colors.green,
[LogLevel.DEBUG]: colors.blue
};
// 寫入檔案日誌
function writeToFile(level: LogLevel, message: string, data?: any) {
const timestamp = new Date().toISOString();
const levelName = levelNames[level];
const logEntry = {
timestamp,
level: levelName,
message,
data: data || null
};
const logLine = JSON.stringify(logEntry) + '\n';
try {
fs.appendFileSync(LOG_FILE, logLine);
} catch (error) {
console.error('Failed to write to log file:', error);
}
}
// 控制台輸出
function consoleOutput(level: LogLevel, message: string, data?: any) {
const timestamp = new Date().toISOString();
const levelName = levelNames[level];
const color = levelColors[level];
let output = `${color}[${levelName}]${colors.reset} ${timestamp} - ${message}`;
if (data) {
output += `\n${color}Data:${colors.reset} ${JSON.stringify(data, null, 2)}`;
}
console.log(output);
}
// 主日誌函數
function log(level: LogLevel, message: string, data?: any) {
if (level <= LOG_LEVEL) {
consoleOutput(level, message, data);
writeToFile(level, message, data);
}
}
// 日誌類別
export class Logger {
private context: string;
constructor(context: string = 'App') {
this.context = context;
}
error(message: string, data?: any) {
log(LogLevel.ERROR, `[${this.context}] ${message}`, data);
}
warn(message: string, data?: any) {
log(LogLevel.WARN, `[${this.context}] ${message}`, data);
}
info(message: string, data?: any) {
log(LogLevel.INFO, `[${this.context}] ${message}`, data);
}
debug(message: string, data?: any) {
log(LogLevel.DEBUG, `[${this.context}] ${message}`, data);
}
// API 請求日誌
logRequest(method: string, url: string, statusCode: number, duration: number, userId?: string) {
this.info('API Request', {
method,
url,
statusCode,
duration: `${duration}ms`,
userId
});
}
// 認證日誌
logAuth(action: string, email: string, success: boolean, ip?: string) {
this.info('Authentication', {
action,
email,
success,
ip
});
}
// 資料庫操作日誌
logDatabase(operation: string, table: string, duration: number, success: boolean) {
this.debug('Database Operation', {
operation,
table,
duration: `${duration}ms`,
success
});
}
// 錯誤日誌
logError(error: Error, context?: string) {
this.error('Application Error', {
message: error.message,
stack: error.stack,
context: context || this.context
});
}
}
// 預設日誌實例
export const logger = new Logger();
// 建立特定上下文的日誌實例
export function createLogger(context: string): Logger {
return new Logger(context);
}

View File

@@ -6,7 +6,11 @@
"build": "next build", "build": "next build",
"dev": "next dev", "dev": "next dev",
"lint": "next lint", "lint": "next lint",
"start": "next start" "start": "next start",
"db:setup": "node scripts/setup-database.js",
"db:test": "node database_connection_test.js",
"db:reset": "node scripts/reset-database.js",
"admin:create": "node scripts/create-admin.js"
}, },
"dependencies": { "dependencies": {
"@hookform/resolvers": "^3.9.1", "@hookform/resolvers": "^3.9.1",
@@ -38,14 +42,18 @@
"@radix-ui/react-toggle-group": "1.1.1", "@radix-ui/react-toggle-group": "1.1.1",
"@radix-ui/react-tooltip": "1.1.6", "@radix-ui/react-tooltip": "1.1.6",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"bcrypt": "^6.0.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "1.0.4", "cmdk": "1.0.4",
"crypto": "^1.0.1",
"date-fns": "4.1.0", "date-fns": "4.1.0",
"embla-carousel-react": "8.5.1", "embla-carousel-react": "8.5.1",
"geist": "^1.3.1", "geist": "^1.3.1",
"input-otp": "1.4.1", "input-otp": "1.4.1",
"jsonwebtoken": "^9.0.2",
"lucide-react": "^0.454.0", "lucide-react": "^0.454.0",
"mysql2": "^3.9.2",
"next": "15.2.4", "next": "15.2.4",
"next-themes": "^0.4.4", "next-themes": "^0.4.4",
"react": "^19", "react": "^19",
@@ -61,6 +69,8 @@
"zod": "^3.24.1" "zod": "^3.24.1"
}, },
"devDependencies": { "devDependencies": {
"@types/bcrypt": "^5.0.2",
"@types/jsonwebtoken": "^9.0.6",
"@types/node": "^22", "@types/node": "^22",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",

248
pnpm-lock.yaml generated
View File

@@ -95,6 +95,9 @@ importers:
autoprefixer: autoprefixer:
specifier: ^10.4.20 specifier: ^10.4.20
version: 10.4.21(postcss@8.5.6) version: 10.4.21(postcss@8.5.6)
bcrypt:
specifier: ^6.0.0
version: 6.0.0
class-variance-authority: class-variance-authority:
specifier: ^0.7.1 specifier: ^0.7.1
version: 0.7.1 version: 0.7.1
@@ -104,6 +107,9 @@ importers:
cmdk: cmdk:
specifier: 1.0.4 specifier: 1.0.4
version: 1.0.4(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) version: 1.0.4(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
crypto:
specifier: ^1.0.1
version: 1.0.1
date-fns: date-fns:
specifier: 4.1.0 specifier: 4.1.0
version: 4.1.0 version: 4.1.0
@@ -116,9 +122,15 @@ importers:
input-otp: input-otp:
specifier: 1.4.1 specifier: 1.4.1
version: 1.4.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1) version: 1.4.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
jsonwebtoken:
specifier: ^9.0.2
version: 9.0.2
lucide-react: lucide-react:
specifier: ^0.454.0 specifier: ^0.454.0
version: 0.454.0(react@19.1.1) version: 0.454.0(react@19.1.1)
mysql2:
specifier: ^3.9.2
version: 3.14.3
next: next:
specifier: 15.2.4 specifier: 15.2.4
version: 15.2.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1) version: 15.2.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
@@ -159,6 +171,12 @@ importers:
specifier: ^3.24.1 specifier: ^3.24.1
version: 3.25.76 version: 3.25.76
devDependencies: devDependencies:
'@types/bcrypt':
specifier: ^5.0.2
version: 5.0.2
'@types/jsonwebtoken':
specifier: ^9.0.6
version: 9.0.10
'@types/node': '@types/node':
specifier: ^22 specifier: ^22
version: 22.17.0 version: 22.17.0
@@ -1281,6 +1299,9 @@ packages:
'@swc/helpers@0.5.15': '@swc/helpers@0.5.15':
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
'@types/bcrypt@5.0.2':
resolution: {integrity: sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==}
'@types/d3-array@3.2.1': '@types/d3-array@3.2.1':
resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==} resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==}
@@ -1308,6 +1329,12 @@ packages:
'@types/d3-timer@3.0.2': '@types/d3-timer@3.0.2':
resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==}
'@types/jsonwebtoken@9.0.10':
resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==}
'@types/ms@2.1.0':
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
'@types/node@22.17.0': '@types/node@22.17.0':
resolution: {integrity: sha512-bbAKTCqX5aNVryi7qXVMi+OkB3w/OyblodicMbvE38blyAz7GxXf6XYhklokijuPwwVg9sDLKRxt0ZHXQwZVfQ==} resolution: {integrity: sha512-bbAKTCqX5aNVryi7qXVMi+OkB3w/OyblodicMbvE38blyAz7GxXf6XYhklokijuPwwVg9sDLKRxt0ZHXQwZVfQ==}
@@ -1359,9 +1386,17 @@ packages:
peerDependencies: peerDependencies:
postcss: ^8.1.0 postcss: ^8.1.0
aws-ssl-profiles@1.1.2:
resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==}
engines: {node: '>= 6.0.0'}
balanced-match@1.0.2: balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
bcrypt@6.0.0:
resolution: {integrity: sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==}
engines: {node: '>= 18'}
binary-extensions@2.3.0: binary-extensions@2.3.0:
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -1378,6 +1413,9 @@ packages:
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true hasBin: true
buffer-equal-constant-time@1.0.1:
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
busboy@1.6.0: busboy@1.6.0:
resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
engines: {node: '>=10.16.0'} engines: {node: '>=10.16.0'}
@@ -1431,6 +1469,10 @@ packages:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
crypto@1.0.1:
resolution: {integrity: sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==}
deprecated: This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in.
cssesc@3.0.0: cssesc@3.0.0:
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
engines: {node: '>=4'} engines: {node: '>=4'}
@@ -1492,6 +1534,10 @@ packages:
decimal.js-light@2.5.1: decimal.js-light@2.5.1:
resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==}
denque@2.1.0:
resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==}
engines: {node: '>=0.10'}
detect-libc@2.0.4: detect-libc@2.0.4:
resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -1508,6 +1554,9 @@ packages:
eastasianwidth@0.2.0: eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
ecdsa-sig-formatter@1.0.11:
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
electron-to-chromium@1.5.193: electron-to-chromium@1.5.193:
resolution: {integrity: sha512-eePuBZXM9OVCwfYUhd2OzESeNGnWmLyeu0XAEjf7xjijNjHFdeJSzuRUGN4ueT2tEYo5YqjHramKEFxz67p3XA==} resolution: {integrity: sha512-eePuBZXM9OVCwfYUhd2OzESeNGnWmLyeu0XAEjf7xjijNjHFdeJSzuRUGN4ueT2tEYo5YqjHramKEFxz67p3XA==}
@@ -1571,6 +1620,9 @@ packages:
peerDependencies: peerDependencies:
next: '>=13.2.0' next: '>=13.2.0'
generate-function@2.3.1:
resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==}
get-nonce@1.0.1: get-nonce@1.0.1:
resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
engines: {node: '>=6'} engines: {node: '>=6'}
@@ -1591,6 +1643,10 @@ packages:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
iconv-lite@0.6.3:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'}
immer@10.1.1: immer@10.1.1:
resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==} resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==}
@@ -1631,6 +1687,9 @@ packages:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'} engines: {node: '>=0.12.0'}
is-property@1.0.2:
resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==}
isexe@2.0.0: isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
@@ -1641,6 +1700,16 @@ packages:
resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==}
hasBin: true hasBin: true
jsonwebtoken@9.0.2:
resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==}
engines: {node: '>=12', npm: '>=6'}
jwa@1.4.2:
resolution: {integrity: sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==}
jws@3.2.2:
resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==}
lilconfig@3.1.3: lilconfig@3.1.3:
resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
engines: {node: '>=14'} engines: {node: '>=14'}
@@ -1648,9 +1717,41 @@ packages:
lines-and-columns@1.2.4: lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
lodash.includes@4.3.0:
resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
lodash.isboolean@3.0.3:
resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==}
lodash.isinteger@4.0.4:
resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==}
lodash.isnumber@3.0.3:
resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==}
lodash.isplainobject@4.0.6:
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
lodash.isstring@4.0.1:
resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==}
lodash.once@4.1.1:
resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
long@5.3.2:
resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==}
lru-cache@10.4.3: lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
lru-cache@7.18.3:
resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==}
engines: {node: '>=12'}
lru.min@1.1.2:
resolution: {integrity: sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg==}
engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'}
lucide-react@0.454.0: lucide-react@0.454.0:
resolution: {integrity: sha512-hw7zMDwykCLnEzgncEEjHeA6+45aeEzRYuKHuyRSOPkhko+J3ySGjGIzu+mmMfDFG1vazHepMaYFYHbTFAZAAQ==} resolution: {integrity: sha512-hw7zMDwykCLnEzgncEEjHeA6+45aeEzRYuKHuyRSOPkhko+J3ySGjGIzu+mmMfDFG1vazHepMaYFYHbTFAZAAQ==}
peerDependencies: peerDependencies:
@@ -1672,9 +1773,20 @@ packages:
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
engines: {node: '>=16 || 14 >=14.17'} engines: {node: '>=16 || 14 >=14.17'}
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
mysql2@3.14.3:
resolution: {integrity: sha512-fD6MLV8XJ1KiNFIF0bS7Msl8eZyhlTDCDl75ajU5SJtpdx9ZPEACulJcqJWr1Y8OYyxsFc4j3+nflpmhxCU5aQ==}
engines: {node: '>= 8.0'}
mz@2.7.0: mz@2.7.0:
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
named-placeholders@1.1.3:
resolution: {integrity: sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==}
engines: {node: '>=12.0.0'}
nanoid@3.3.11: nanoid@3.3.11:
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
@@ -1707,6 +1819,14 @@ packages:
sass: sass:
optional: true optional: true
node-addon-api@8.5.0:
resolution: {integrity: sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==}
engines: {node: ^18 || ^20 || >= 21}
node-gyp-build@4.8.4:
resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
hasBin: true
node-releases@2.0.19: node-releases@2.0.19:
resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
@@ -1913,6 +2033,12 @@ packages:
run-parallel@1.2.0: run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
scheduler@0.26.0: scheduler@0.26.0:
resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==}
@@ -1921,6 +2047,9 @@ packages:
engines: {node: '>=10'} engines: {node: '>=10'}
hasBin: true hasBin: true
seq-queue@0.0.5:
resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==}
sharp@0.33.5: sharp@0.33.5:
resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
@@ -1950,6 +2079,10 @@ packages:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
sqlstring@2.3.3:
resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==}
engines: {node: '>= 0.6'}
streamsearch@1.1.0: streamsearch@1.1.0:
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
engines: {node: '>=10.0.0'} engines: {node: '>=10.0.0'}
@@ -3146,6 +3279,10 @@ snapshots:
dependencies: dependencies:
tslib: 2.8.1 tslib: 2.8.1
'@types/bcrypt@5.0.2':
dependencies:
'@types/node': 22.17.0
'@types/d3-array@3.2.1': {} '@types/d3-array@3.2.1': {}
'@types/d3-color@3.1.3': {} '@types/d3-color@3.1.3': {}
@@ -3170,6 +3307,13 @@ snapshots:
'@types/d3-timer@3.0.2': {} '@types/d3-timer@3.0.2': {}
'@types/jsonwebtoken@9.0.10':
dependencies:
'@types/ms': 2.1.0
'@types/node': 22.17.0
'@types/ms@2.1.0': {}
'@types/node@22.17.0': '@types/node@22.17.0':
dependencies: dependencies:
undici-types: 6.21.0 undici-types: 6.21.0
@@ -3217,8 +3361,15 @@ snapshots:
postcss: 8.5.6 postcss: 8.5.6
postcss-value-parser: 4.2.0 postcss-value-parser: 4.2.0
aws-ssl-profiles@1.1.2: {}
balanced-match@1.0.2: {} balanced-match@1.0.2: {}
bcrypt@6.0.0:
dependencies:
node-addon-api: 8.5.0
node-gyp-build: 4.8.4
binary-extensions@2.3.0: {} binary-extensions@2.3.0: {}
brace-expansion@2.0.2: brace-expansion@2.0.2:
@@ -3236,6 +3387,8 @@ snapshots:
node-releases: 2.0.19 node-releases: 2.0.19
update-browserslist-db: 1.1.3(browserslist@4.25.1) update-browserslist-db: 1.1.3(browserslist@4.25.1)
buffer-equal-constant-time@1.0.1: {}
busboy@1.6.0: busboy@1.6.0:
dependencies: dependencies:
streamsearch: 1.1.0 streamsearch: 1.1.0
@@ -3302,6 +3455,8 @@ snapshots:
shebang-command: 2.0.0 shebang-command: 2.0.0
which: 2.0.2 which: 2.0.2
crypto@1.0.1: {}
cssesc@3.0.0: {} cssesc@3.0.0: {}
csstype@3.1.3: {} csstype@3.1.3: {}
@@ -3350,6 +3505,8 @@ snapshots:
decimal.js-light@2.5.1: {} decimal.js-light@2.5.1: {}
denque@2.1.0: {}
detect-libc@2.0.4: detect-libc@2.0.4:
optional: true optional: true
@@ -3361,6 +3518,10 @@ snapshots:
eastasianwidth@0.2.0: {} eastasianwidth@0.2.0: {}
ecdsa-sig-formatter@1.0.11:
dependencies:
safe-buffer: 5.2.1
electron-to-chromium@1.5.193: {} electron-to-chromium@1.5.193: {}
embla-carousel-react@8.5.1(react@19.1.1): embla-carousel-react@8.5.1(react@19.1.1):
@@ -3417,6 +3578,10 @@ snapshots:
dependencies: dependencies:
next: 15.2.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1) next: 15.2.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
generate-function@2.3.1:
dependencies:
is-property: 1.0.2
get-nonce@1.0.1: {} get-nonce@1.0.1: {}
glob-parent@5.1.2: glob-parent@5.1.2:
@@ -3440,6 +3605,10 @@ snapshots:
dependencies: dependencies:
function-bind: 1.1.2 function-bind: 1.1.2
iconv-lite@0.6.3:
dependencies:
safer-buffer: 2.1.2
immer@10.1.1: {} immer@10.1.1: {}
input-otp@1.4.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1): input-otp@1.4.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1):
@@ -3470,6 +3639,8 @@ snapshots:
is-number@7.0.0: {} is-number@7.0.0: {}
is-property@1.0.2: {}
isexe@2.0.0: {} isexe@2.0.0: {}
jackspeak@3.4.3: jackspeak@3.4.3:
@@ -3480,12 +3651,56 @@ snapshots:
jiti@1.21.7: {} jiti@1.21.7: {}
jsonwebtoken@9.0.2:
dependencies:
jws: 3.2.2
lodash.includes: 4.3.0
lodash.isboolean: 3.0.3
lodash.isinteger: 4.0.4
lodash.isnumber: 3.0.3
lodash.isplainobject: 4.0.6
lodash.isstring: 4.0.1
lodash.once: 4.1.1
ms: 2.1.3
semver: 7.7.2
jwa@1.4.2:
dependencies:
buffer-equal-constant-time: 1.0.1
ecdsa-sig-formatter: 1.0.11
safe-buffer: 5.2.1
jws@3.2.2:
dependencies:
jwa: 1.4.2
safe-buffer: 5.2.1
lilconfig@3.1.3: {} lilconfig@3.1.3: {}
lines-and-columns@1.2.4: {} lines-and-columns@1.2.4: {}
lodash.includes@4.3.0: {}
lodash.isboolean@3.0.3: {}
lodash.isinteger@4.0.4: {}
lodash.isnumber@3.0.3: {}
lodash.isplainobject@4.0.6: {}
lodash.isstring@4.0.1: {}
lodash.once@4.1.1: {}
long@5.3.2: {}
lru-cache@10.4.3: {} lru-cache@10.4.3: {}
lru-cache@7.18.3: {}
lru.min@1.1.2: {}
lucide-react@0.454.0(react@19.1.1): lucide-react@0.454.0(react@19.1.1):
dependencies: dependencies:
react: 19.1.1 react: 19.1.1
@@ -3503,12 +3718,30 @@ snapshots:
minipass@7.1.2: {} minipass@7.1.2: {}
ms@2.1.3: {}
mysql2@3.14.3:
dependencies:
aws-ssl-profiles: 1.1.2
denque: 2.1.0
generate-function: 2.3.1
iconv-lite: 0.6.3
long: 5.3.2
lru.min: 1.1.2
named-placeholders: 1.1.3
seq-queue: 0.0.5
sqlstring: 2.3.3
mz@2.7.0: mz@2.7.0:
dependencies: dependencies:
any-promise: 1.3.0 any-promise: 1.3.0
object-assign: 4.1.1 object-assign: 4.1.1
thenify-all: 1.6.0 thenify-all: 1.6.0
named-placeholders@1.1.3:
dependencies:
lru-cache: 7.18.3
nanoid@3.3.11: {} nanoid@3.3.11: {}
next-themes@0.4.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1): next-themes@0.4.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1):
@@ -3541,6 +3774,10 @@ snapshots:
- '@babel/core' - '@babel/core'
- babel-plugin-macros - babel-plugin-macros
node-addon-api@8.5.0: {}
node-gyp-build@4.8.4: {}
node-releases@2.0.19: {} node-releases@2.0.19: {}
normalize-path@3.0.0: {} normalize-path@3.0.0: {}
@@ -3724,10 +3961,15 @@ snapshots:
dependencies: dependencies:
queue-microtask: 1.2.3 queue-microtask: 1.2.3
safe-buffer@5.2.1: {}
safer-buffer@2.1.2: {}
scheduler@0.26.0: {} scheduler@0.26.0: {}
semver@7.7.2: semver@7.7.2: {}
optional: true
seq-queue@0.0.5: {}
sharp@0.33.5: sharp@0.33.5:
dependencies: dependencies:
@@ -3776,6 +4018,8 @@ snapshots:
source-map-js@1.2.1: {} source-map-js@1.2.1: {}
sqlstring@2.3.3: {}
streamsearch@1.1.0: {} streamsearch@1.1.0: {}
string-width@4.2.3: string-width@4.2.3:

2
pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,2 @@
ignoredBuiltDependencies:
- bcrypt

134
scripts/create-admin.js Normal file
View File

@@ -0,0 +1,134 @@
const mysql = require('mysql2/promise');
const bcrypt = require('bcrypt');
const crypto = require('crypto');
// 資料庫配置
const dbConfig = {
host: process.env.DB_HOST || 'mysql.theaken.com',
port: parseInt(process.env.DB_PORT || '33306'),
user: process.env.DB_USER || 'AI_Platform',
password: process.env.DB_PASSWORD || 'Aa123456',
database: process.env.DB_NAME || 'db_AI_Platform',
charset: 'utf8mb4',
timezone: '+08:00'
};
// 生成 UUID
function generateId() {
return crypto.randomUUID();
}
// 加密密碼
async function hashPassword(password) {
const saltRounds = 12;
return await bcrypt.hash(password, saltRounds);
}
async function createAdmin() {
let connection;
try {
console.log('🔌 連接資料庫...');
connection = await mysql.createConnection(dbConfig);
console.log('✅ 資料庫連接成功');
// 管理員資料
const adminData = {
id: generateId(),
name: 'AI平台管理員',
email: 'admin@theaken.com',
password: 'Admin@2024',
department: '資訊技術部',
role: 'admin'
};
console.log('\n📋 準備建立管理員帳號:');
console.log(` 姓名: ${adminData.name}`);
console.log(` 電子郵件: ${adminData.email}`);
console.log(` 部門: ${adminData.department}`);
console.log(` 角色: ${adminData.role}`);
// 檢查是否已存在
const [existingUser] = await connection.execute(
'SELECT id FROM users WHERE email = ?',
[adminData.email]
);
if (existingUser.length > 0) {
console.log('\n⚠ 管理員帳號已存在,更新密碼...');
// 加密新密碼
const passwordHash = await hashPassword(adminData.password);
// 更新密碼
await connection.execute(
'UPDATE users SET password_hash = ?, updated_at = NOW() WHERE email = ?',
[passwordHash, adminData.email]
);
console.log('✅ 管理員密碼已更新');
} else {
console.log('\n📝 建立新的管理員帳號...');
// 加密密碼
const passwordHash = await hashPassword(adminData.password);
// 插入管理員資料
await connection.execute(`
INSERT INTO users (
id, name, email, password_hash, department, role,
join_date, total_likes, total_views, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, CURDATE(), 0, 0, NOW(), NOW())
`, [
adminData.id,
adminData.name,
adminData.email,
passwordHash,
adminData.department,
adminData.role
]);
console.log('✅ 管理員帳號建立成功');
}
// 驗證建立結果
console.log('\n🔍 驗證管理員帳號...');
const [adminUser] = await connection.execute(
'SELECT id, name, email, department, role, created_at FROM users WHERE email = ?',
[adminData.email]
);
if (adminUser.length > 0) {
const user = adminUser[0];
console.log('✅ 管理員帳號驗證成功:');
console.log(` ID: ${user.id}`);
console.log(` 姓名: ${user.name}`);
console.log(` 電子郵件: ${user.email}`);
console.log(` 部門: ${user.department}`);
console.log(` 角色: ${user.role}`);
console.log(` 建立時間: ${user.created_at}`);
}
console.log('\n🎉 管理員帳號設定完成!');
console.log('\n📋 登入資訊:');
console.log(` 電子郵件: ${adminData.email}`);
console.log(` 密碼: ${adminData.password}`);
console.log('\n⚠ 請妥善保管登入資訊,建議在首次登入後更改密碼');
} catch (error) {
console.error('❌ 建立管理員帳號失敗:', error.message);
process.exit(1);
} finally {
if (connection) {
await connection.end();
console.log('🔌 資料庫連接已關閉');
}
}
}
// 如果直接執行此腳本
if (require.main === module) {
createAdmin();
}
module.exports = { createAdmin };

136
scripts/fix-tables.js Normal file
View File

@@ -0,0 +1,136 @@
const mysql = require('mysql2/promise');
// 資料庫配置
const dbConfig = {
host: process.env.DB_HOST || 'mysql.theaken.com',
port: parseInt(process.env.DB_PORT || '33306'),
user: process.env.DB_USER || 'AI_Platform',
password: process.env.DB_PASSWORD || 'Aa123456',
database: process.env.DB_NAME || 'db_AI_Platform',
charset: 'utf8mb4',
timezone: '+08:00'
};
async function fixTables() {
let connection;
try {
console.log('🔧 修復剩餘的資料表...');
// 連接資料庫
connection = await mysql.createConnection(dbConfig);
// 修復 awards 表 (rank 是保留字)
console.log('修復 awards 表...');
const awardsTable = `
CREATE TABLE IF NOT EXISTS awards (
id VARCHAR(36) PRIMARY KEY,
competition_id VARCHAR(36) NOT NULL,
app_id VARCHAR(36),
team_id VARCHAR(36),
proposal_id VARCHAR(36),
award_type ENUM('gold', 'silver', 'bronze', 'popular', 'innovation', 'technical', 'custom') NOT NULL,
award_name VARCHAR(200) NOT NULL,
score DECIMAL(5,2) NOT NULL,
year INT NOT NULL,
month INT NOT NULL,
icon VARCHAR(50),
custom_award_type_id VARCHAR(36),
competition_type ENUM('individual', 'team', 'proposal') NOT NULL,
award_rank INT DEFAULT 0,
category ENUM('innovation', 'technical', 'practical', 'popular', 'teamwork', 'solution', 'creativity') NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (competition_id) REFERENCES competitions(id) ON DELETE CASCADE,
FOREIGN KEY (app_id) REFERENCES apps(id) ON DELETE SET NULL,
FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE SET NULL,
FOREIGN KEY (proposal_id) REFERENCES proposals(id) ON DELETE SET NULL,
INDEX idx_competition (competition_id),
INDEX idx_award_type (award_type),
INDEX idx_year_month (year, month),
INDEX idx_category (category)
)
`;
await connection.query(awardsTable);
console.log('✅ awards 表建立成功');
// 修復 user_likes 表 (DATE() 函數在唯一約束中的問題)
console.log('修復 user_likes 表...');
const userLikesTable = `
CREATE TABLE IF NOT EXISTS user_likes (
id VARCHAR(36) PRIMARY KEY,
user_id VARCHAR(36) NOT NULL,
app_id VARCHAR(36),
proposal_id VARCHAR(36),
liked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (app_id) REFERENCES apps(id) ON DELETE CASCADE,
FOREIGN KEY (proposal_id) REFERENCES proposals(id) ON DELETE CASCADE,
UNIQUE KEY unique_user_app_date (user_id, app_id, DATE(liked_at)),
UNIQUE KEY unique_user_proposal_date (user_id, proposal_id, DATE(liked_at)),
INDEX idx_user (user_id),
INDEX idx_app (app_id),
INDEX idx_proposal (proposal_id),
INDEX idx_date (liked_at)
)
`;
await connection.query(userLikesTable);
console.log('✅ user_likes 表建立成功');
// 驗證修復結果
console.log('\n📋 驗證修復結果...');
const [tables] = await connection.query(`
SELECT TABLE_NAME, TABLE_ROWS
FROM information_schema.tables
WHERE table_schema = '${dbConfig.database}'
ORDER BY TABLE_NAME
`);
console.log('\n📊 資料表列表:');
console.log('─'.repeat(60));
console.log('表名'.padEnd(25) + '| 記錄數'.padEnd(10) + '| 狀態');
console.log('─'.repeat(60));
const expectedTables = [
'users', 'competitions', 'judges', 'teams', 'team_members',
'apps', 'proposals', 'judge_scores', 'awards', 'chat_sessions',
'chat_messages', 'ai_assistant_configs', 'user_favorites',
'user_likes', 'competition_participants', 'competition_judges',
'system_settings', 'activity_logs'
];
let successCount = 0;
for (const expectedTable of expectedTables) {
const table = tables.find(t => t.TABLE_NAME === expectedTable);
const status = table ? '✅' : '❌';
const rowCount = table ? (table.TABLE_ROWS || 0) : 'N/A';
console.log(`${expectedTable.padEnd(25)}| ${rowCount.toString().padEnd(10)}| ${status}`);
if (table) successCount++;
}
console.log(`\n📊 成功建立 ${successCount}/${expectedTables.length} 個資料表`);
if (successCount === expectedTables.length) {
console.log('🎉 所有資料表建立完成!');
} else {
console.log('⚠️ 仍有部分資料表未建立');
}
} catch (error) {
console.error('❌ 修復資料表失敗:', error.message);
} finally {
if (connection) {
await connection.end();
console.log('\n🔌 資料庫連接已關閉');
}
}
}
// 執行修復腳本
if (require.main === module) {
fixTables();
}
module.exports = { fixTables };

80
scripts/fix-user-likes.js Normal file
View File

@@ -0,0 +1,80 @@
const mysql = require('mysql2/promise');
// 資料庫配置
const dbConfig = {
host: process.env.DB_HOST || 'mysql.theaken.com',
port: parseInt(process.env.DB_PORT || '33306'),
user: process.env.DB_USER || 'AI_Platform',
password: process.env.DB_PASSWORD || 'Aa123456',
database: process.env.DB_NAME || 'db_AI_Platform',
charset: 'utf8mb4',
timezone: '+08:00'
};
async function fixUserLikes() {
let connection;
try {
console.log('🔧 修復 user_likes 表...');
// 連接資料庫
connection = await mysql.createConnection(dbConfig);
// 先刪除可能存在的表
try {
await connection.query('DROP TABLE IF EXISTS user_likes');
console.log('✅ 刪除舊的 user_likes 表');
} catch (error) {
console.log('沒有舊表需要刪除');
}
// 建立簡化版的 user_likes 表
const userLikesTable = `
CREATE TABLE user_likes (
id VARCHAR(36) PRIMARY KEY,
user_id VARCHAR(36) NOT NULL,
app_id VARCHAR(36),
proposal_id VARCHAR(36),
liked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (app_id) REFERENCES apps(id) ON DELETE CASCADE,
FOREIGN KEY (proposal_id) REFERENCES proposals(id) ON DELETE CASCADE,
INDEX idx_user (user_id),
INDEX idx_app (app_id),
INDEX idx_proposal (proposal_id),
INDEX idx_date (liked_at)
)
`;
await connection.query(userLikesTable);
console.log('✅ user_likes 表建立成功');
// 驗證結果
const [tables] = await connection.query(`
SELECT TABLE_NAME, TABLE_ROWS
FROM information_schema.tables
WHERE table_schema = '${dbConfig.database}' AND TABLE_NAME = 'user_likes'
`);
if (tables.length > 0) {
console.log('✅ user_likes 表驗證成功');
} else {
console.log('❌ user_likes 表建立失敗');
}
} catch (error) {
console.error('❌ 修復 user_likes 表失敗:', error.message);
} finally {
if (connection) {
await connection.end();
console.log('\n🔌 資料庫連接已關閉');
}
}
}
// 執行修復腳本
if (require.main === module) {
fixUserLikes();
}
module.exports = { fixUserLikes };

View File

@@ -0,0 +1,491 @@
const mysql = require('mysql2/promise');
// 資料庫配置
const dbConfig = {
host: process.env.DB_HOST || 'mysql.theaken.com',
port: parseInt(process.env.DB_PORT || '33306'),
user: process.env.DB_USER || 'AI_Platform',
password: process.env.DB_PASSWORD || 'Aa123456',
database: process.env.DB_NAME || 'db_AI_Platform',
charset: 'utf8mb4',
timezone: '+08:00'
};
async function setupDatabase() {
let connection;
try {
console.log('🚀 開始建立AI展示平台資料庫...');
console.log('─'.repeat(50));
// 1. 連接資料庫
console.log('🔌 連接資料庫...');
connection = await mysql.createConnection({
...dbConfig,
database: undefined
});
// 2. 檢查資料庫是否存在
const [databases] = await connection.query('SHOW DATABASES');
const dbExists = databases.some(db => db.Database === dbConfig.database);
if (!dbExists) {
console.log(`📊 建立資料庫: ${dbConfig.database}`);
await connection.query(`CREATE DATABASE ${dbConfig.database} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci`);
} else {
console.log(`✅ 資料庫已存在: ${dbConfig.database}`);
}
// 3. 切換到目標資料庫
await connection.query(`USE ${dbConfig.database}`);
// 4. 手動執行SQL語句
console.log('📝 執行資料庫建立腳本...');
const sqlStatements = [
// 1. 用戶表
`CREATE TABLE IF NOT EXISTS users (
id VARCHAR(36) PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
avatar VARCHAR(500),
department VARCHAR(100) NOT NULL,
role ENUM('user', 'developer', 'admin') DEFAULT 'user',
join_date DATE NOT NULL,
total_likes INT DEFAULT 0,
total_views INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_email (email),
INDEX idx_department (department),
INDEX idx_role (role)
)`,
// 2. 競賽表
`CREATE TABLE IF NOT EXISTS competitions (
id VARCHAR(36) PRIMARY KEY,
name VARCHAR(200) NOT NULL,
year INT NOT NULL,
month INT NOT NULL,
start_date DATE NOT NULL,
end_date DATE NOT NULL,
status ENUM('upcoming', 'active', 'judging', 'completed') DEFAULT 'upcoming',
description TEXT,
type ENUM('individual', 'team', 'mixed', 'proposal') NOT NULL,
evaluation_focus TEXT,
max_team_size INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_status (status),
INDEX idx_type (type),
INDEX idx_year_month (year, month),
INDEX idx_dates (start_date, end_date)
)`,
// 3. 評審表
`CREATE TABLE IF NOT EXISTS judges (
id VARCHAR(36) PRIMARY KEY,
name VARCHAR(100) NOT NULL,
title VARCHAR(100) NOT NULL,
department VARCHAR(100) NOT NULL,
expertise JSON,
avatar VARCHAR(500),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_department (department)
)`,
// 4. 團隊表
`CREATE TABLE IF NOT EXISTS teams (
id VARCHAR(36) PRIMARY KEY,
name VARCHAR(200) NOT NULL,
leader_id VARCHAR(36) NOT NULL,
department VARCHAR(100) NOT NULL,
contact_email VARCHAR(255) NOT NULL,
total_likes INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (leader_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_department (department),
INDEX idx_leader (leader_id)
)`,
// 5. 團隊成員表
`CREATE TABLE IF NOT EXISTS team_members (
id VARCHAR(36) PRIMARY KEY,
team_id VARCHAR(36) NOT NULL,
user_id VARCHAR(36) NOT NULL,
role VARCHAR(50) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
UNIQUE KEY unique_team_user (team_id, user_id),
INDEX idx_team (team_id),
INDEX idx_user (user_id)
)`,
// 6. 應用表
`CREATE TABLE IF NOT EXISTS apps (
id VARCHAR(36) PRIMARY KEY,
name VARCHAR(200) NOT NULL,
description TEXT,
creator_id VARCHAR(36) NOT NULL,
team_id VARCHAR(36),
likes_count INT DEFAULT 0,
views_count INT DEFAULT 0,
rating DECIMAL(3,2) DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (creator_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE SET NULL,
INDEX idx_creator (creator_id),
INDEX idx_team (team_id),
INDEX idx_rating (rating),
INDEX idx_likes (likes_count)
)`,
// 7. 提案表
`CREATE TABLE IF NOT EXISTS proposals (
id VARCHAR(36) PRIMARY KEY,
title VARCHAR(200) NOT NULL,
description TEXT,
creator_id VARCHAR(36) NOT NULL,
team_id VARCHAR(36),
status ENUM('draft', 'submitted', 'under_review', 'approved', 'rejected') DEFAULT 'draft',
likes_count INT DEFAULT 0,
views_count INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (creator_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE SET NULL,
INDEX idx_creator (creator_id),
INDEX idx_status (status)
)`,
// 8. 評分表
`CREATE TABLE IF NOT EXISTS judge_scores (
id VARCHAR(36) PRIMARY KEY,
judge_id VARCHAR(36) NOT NULL,
app_id VARCHAR(36),
proposal_id VARCHAR(36),
scores JSON NOT NULL,
comments TEXT,
submitted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (judge_id) REFERENCES judges(id) ON DELETE CASCADE,
FOREIGN KEY (app_id) REFERENCES apps(id) ON DELETE CASCADE,
FOREIGN KEY (proposal_id) REFERENCES proposals(id) ON DELETE CASCADE,
UNIQUE KEY unique_judge_app (judge_id, app_id),
UNIQUE KEY unique_judge_proposal (judge_id, proposal_id),
INDEX idx_judge (judge_id),
INDEX idx_app (app_id),
INDEX idx_proposal (proposal_id)
)`,
// 9. 獎項表
`CREATE TABLE IF NOT EXISTS awards (
id VARCHAR(36) PRIMARY KEY,
competition_id VARCHAR(36) NOT NULL,
app_id VARCHAR(36),
team_id VARCHAR(36),
proposal_id VARCHAR(36),
award_type ENUM('gold', 'silver', 'bronze', 'popular', 'innovation', 'technical', 'custom') NOT NULL,
award_name VARCHAR(200) NOT NULL,
score DECIMAL(5,2) NOT NULL,
year INT NOT NULL,
month INT NOT NULL,
icon VARCHAR(50),
custom_award_type_id VARCHAR(36),
competition_type ENUM('individual', 'team', 'proposal') NOT NULL,
rank INT DEFAULT 0,
category ENUM('innovation', 'technical', 'practical', 'popular', 'teamwork', 'solution', 'creativity') NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (competition_id) REFERENCES competitions(id) ON DELETE CASCADE,
FOREIGN KEY (app_id) REFERENCES apps(id) ON DELETE SET NULL,
FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE SET NULL,
FOREIGN KEY (proposal_id) REFERENCES proposals(id) ON DELETE SET NULL,
INDEX idx_competition (competition_id),
INDEX idx_award_type (award_type),
INDEX idx_year_month (year, month),
INDEX idx_category (category)
)`,
// 10. 聊天會話表
`CREATE TABLE IF NOT EXISTS chat_sessions (
id VARCHAR(36) PRIMARY KEY,
user_id VARCHAR(36) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_user (user_id),
INDEX idx_created (created_at)
)`,
// 11. 聊天訊息表
`CREATE TABLE IF NOT EXISTS chat_messages (
id VARCHAR(36) PRIMARY KEY,
session_id VARCHAR(36) NOT NULL,
text TEXT NOT NULL,
sender ENUM('user', 'bot') NOT NULL,
quick_questions JSON,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (session_id) REFERENCES chat_sessions(id) ON DELETE CASCADE,
INDEX idx_session (session_id),
INDEX idx_created (created_at)
)`,
// 12. AI助手配置表
`CREATE TABLE IF NOT EXISTS ai_assistant_configs (
id VARCHAR(36) PRIMARY KEY,
api_key VARCHAR(255) NOT NULL,
api_url VARCHAR(500) NOT NULL,
model VARCHAR(100) NOT NULL,
max_tokens INT DEFAULT 200,
temperature DECIMAL(3,2) DEFAULT 0.7,
system_prompt TEXT NOT NULL,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_active (is_active)
)`,
// 13. 用戶收藏表
`CREATE TABLE IF NOT EXISTS user_favorites (
id VARCHAR(36) PRIMARY KEY,
user_id VARCHAR(36) NOT NULL,
app_id VARCHAR(36),
proposal_id VARCHAR(36),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (app_id) REFERENCES apps(id) ON DELETE CASCADE,
FOREIGN KEY (proposal_id) REFERENCES proposals(id) ON DELETE CASCADE,
UNIQUE KEY unique_user_app (user_id, app_id),
UNIQUE KEY unique_user_proposal (user_id, proposal_id),
INDEX idx_user (user_id)
)`,
// 14. 用戶按讚表
`CREATE TABLE IF NOT EXISTS user_likes (
id VARCHAR(36) PRIMARY KEY,
user_id VARCHAR(36) NOT NULL,
app_id VARCHAR(36),
proposal_id VARCHAR(36),
liked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (app_id) REFERENCES apps(id) ON DELETE CASCADE,
FOREIGN KEY (proposal_id) REFERENCES proposals(id) ON DELETE CASCADE,
UNIQUE KEY unique_user_app_date (user_id, app_id, DATE(liked_at)),
UNIQUE KEY unique_user_proposal_date (user_id, proposal_id, DATE(liked_at)),
INDEX idx_user (user_id),
INDEX idx_app (app_id),
INDEX idx_proposal (proposal_id),
INDEX idx_date (liked_at)
)`,
// 15. 競賽參與表
`CREATE TABLE IF NOT EXISTS competition_participants (
id VARCHAR(36) PRIMARY KEY,
competition_id VARCHAR(36) NOT NULL,
user_id VARCHAR(36),
team_id VARCHAR(36),
app_id VARCHAR(36),
proposal_id VARCHAR(36),
status ENUM('registered', 'submitted', 'approved', 'rejected') DEFAULT 'registered',
registered_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (competition_id) REFERENCES competitions(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE CASCADE,
FOREIGN KEY (app_id) REFERENCES apps(id) ON DELETE CASCADE,
FOREIGN KEY (proposal_id) REFERENCES proposals(id) ON DELETE CASCADE,
INDEX idx_competition (competition_id),
INDEX idx_user (user_id),
INDEX idx_team (team_id),
INDEX idx_status (status)
)`,
// 16. 競賽評審分配表
`CREATE TABLE IF NOT EXISTS competition_judges (
id VARCHAR(36) PRIMARY KEY,
competition_id VARCHAR(36) NOT NULL,
judge_id VARCHAR(36) NOT NULL,
assigned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (competition_id) REFERENCES competitions(id) ON DELETE CASCADE,
FOREIGN KEY (judge_id) REFERENCES judges(id) ON DELETE CASCADE,
UNIQUE KEY unique_competition_judge (competition_id, judge_id),
INDEX idx_competition (competition_id),
INDEX idx_judge (judge_id)
)`,
// 17. 系統設定表
`CREATE TABLE IF NOT EXISTS system_settings (
id VARCHAR(36) PRIMARY KEY,
setting_key VARCHAR(100) UNIQUE NOT NULL,
setting_value TEXT,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_key (setting_key)
)`,
// 18. 活動日誌表
`CREATE TABLE IF NOT EXISTS activity_logs (
id VARCHAR(36) PRIMARY KEY,
user_id VARCHAR(36),
action VARCHAR(100) NOT NULL,
target_type ENUM('user', 'competition', 'app', 'proposal', 'team', 'award') NOT NULL,
target_id VARCHAR(36),
details JSON,
ip_address VARCHAR(45),
user_agent TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL,
INDEX idx_user (user_id),
INDEX idx_action (action),
INDEX idx_target (target_type, target_id),
INDEX idx_created (created_at)
)`
];
console.log(`📋 準備執行 ${sqlStatements.length} 個SQL語句`);
for (let i = 0; i < sqlStatements.length; i++) {
const statement = sqlStatements[i];
try {
console.log(`執行語句 ${i + 1}/${sqlStatements.length}: ${statement.substring(0, 50)}...`);
await connection.query(statement);
} catch (error) {
console.error(`SQL執行錯誤 (語句 ${i + 1}):`, error.message);
}
}
// 5. 插入初始數據
console.log('\n📝 插入初始數據...');
const insertStatements = [
// 插入預設管理員用戶
`INSERT IGNORE INTO users (id, name, email, password_hash, department, role, join_date) VALUES
('admin-001', '系統管理員', 'admin@theaken.com', '$2b$10$rQZ8K9mN2pL1vX3yU7wE4tA6sB8cD1eF2gH3iJ4kL5mN6oP7qR8sT9uV0wX1yZ2a', '資訊部', 'admin', '2025-01-01')`,
// 插入預設評審
`INSERT IGNORE INTO judges (id, name, title, department, expertise) VALUES
('judge-001', '張教授', '資深技術顧問', '研發部', '["AI", "機器學習", "深度學習"]'),
('judge-002', '李經理', '產品經理', '產品部', '["產品設計", "用戶體驗", "市場分析"]'),
('judge-003', '王工程師', '資深工程師', '技術部', '["軟體開發", "系統架構", "雲端技術"]')`,
// 插入預設競賽
`INSERT IGNORE INTO competitions (id, name, year, month, start_date, end_date, status, description, type, evaluation_focus, max_team_size) VALUES
('comp-2025-01', '2025年AI創新競賽', 2025, 1, '2025-01-15', '2025-03-15', 'upcoming', '年度AI技術創新競賽鼓勵員工開發創新AI應用', 'mixed', '創新性、技術實現、實用價值', 5),
('comp-2025-02', '2025年提案競賽', 2025, 2, '2025-02-01', '2025-04-01', 'upcoming', 'AI解決方案提案競賽', 'proposal', '解決方案可行性、創新程度、商業價值', NULL)`,
// 插入AI助手配置
`INSERT IGNORE INTO ai_assistant_configs (id, api_key, api_url, model, max_tokens, temperature, system_prompt, is_active) VALUES
('ai-config-001', 'your_deepseek_api_key_here', 'https://api.deepseek.com/v1/chat/completions', 'deepseek-chat', 200, 0.7, '你是一個AI展示平台的智能助手專門協助用戶使用平台功能。請用友善、專業的態度回答問題。', TRUE)`,
// 插入系統設定
`INSERT IGNORE INTO system_settings (setting_key, setting_value, description) VALUES
('daily_like_limit', '5', '用戶每日按讚限制'),
('max_team_size', '5', '最大團隊人數'),
('competition_registration_deadline', '7', '競賽報名截止天數'),
('judge_score_weight_innovation', '25', '創新性評分權重'),
('judge_score_weight_technical', '25', '技術性評分權重'),
('judge_score_weight_usability', '20', '實用性評分權重'),
('judge_score_weight_presentation', '15', '展示效果評分權重'),
('judge_score_weight_impact', '15', '影響力評分權重')`
];
for (let i = 0; i < insertStatements.length; i++) {
const statement = insertStatements[i];
try {
console.log(`插入數據 ${i + 1}/${insertStatements.length}...`);
await connection.query(statement);
} catch (error) {
console.error(`插入數據錯誤 (語句 ${i + 1}):`, error.message);
}
}
console.log('✅ 資料庫建立完成!');
// 6. 驗證建立結果
console.log('\n📋 驗證資料庫結構...');
// 檢查資料表
const [tables] = await connection.query(`
SELECT TABLE_NAME, TABLE_ROWS
FROM information_schema.tables
WHERE table_schema = '${dbConfig.database}'
ORDER BY TABLE_NAME
`);
console.log('\n📊 資料表列表:');
console.log('─'.repeat(60));
console.log('表名'.padEnd(25) + '| 記錄數'.padEnd(10) + '| 狀態');
console.log('─'.repeat(60));
const expectedTables = [
'users', 'competitions', 'judges', 'teams', 'team_members',
'apps', 'proposals', 'judge_scores', 'awards', 'chat_sessions',
'chat_messages', 'ai_assistant_configs', 'user_favorites',
'user_likes', 'competition_participants', 'competition_judges',
'system_settings', 'activity_logs'
];
let successCount = 0;
for (const expectedTable of expectedTables) {
const table = tables.find(t => t.TABLE_NAME === expectedTable);
const status = table ? '✅' : '❌';
const rowCount = table ? (table.TABLE_ROWS || 0) : 'N/A';
console.log(`${expectedTable.padEnd(25)}| ${rowCount.toString().padEnd(10)}| ${status}`);
if (table) successCount++;
}
console.log(`\n📊 成功建立 ${successCount}/${expectedTables.length} 個資料表`);
// 檢查初始數據
console.log('\n📊 初始數據檢查:');
console.log('─'.repeat(40));
const checks = [
{ name: '管理員用戶', query: 'SELECT COUNT(*) as count FROM users WHERE role = "admin"' },
{ name: '預設評審', query: 'SELECT COUNT(*) as count FROM judges' },
{ name: '預設競賽', query: 'SELECT COUNT(*) as count FROM competitions' },
{ name: 'AI配置', query: 'SELECT COUNT(*) as count FROM ai_assistant_configs' },
{ name: '系統設定', query: 'SELECT COUNT(*) as count FROM system_settings' }
];
for (const check of checks) {
try {
const [result] = await connection.query(check.query);
console.log(`${check.name.padEnd(15)}: ${result[0].count}`);
} catch (error) {
console.log(`${check.name.padEnd(15)}: 查詢失敗 - ${error.message}`);
}
}
console.log('\n🎉 資料庫建立和驗證完成!');
console.log('\n📝 下一步:');
console.log('1. 複製 env.example 到 .env.local');
console.log('2. 設定環境變數');
console.log('3. 安裝依賴: pnpm install');
console.log('4. 啟動開發服務器: pnpm dev');
} catch (error) {
console.error('❌ 資料庫建立失敗:', error.message);
console.error('請檢查以下項目:');
console.error('1. 資料庫主機是否可達');
console.error('2. 用戶名和密碼是否正確');
console.error('3. 用戶是否有建立資料庫的權限');
process.exit(1);
} finally {
if (connection) {
await connection.end();
console.log('\n🔌 資料庫連接已關閉');
}
}
}
// 執行建立腳本
if (require.main === module) {
setupDatabase();
}
module.exports = { setupDatabase };

160
scripts/setup-database.js Normal file
View File

@@ -0,0 +1,160 @@
const fs = require('fs');
const path = require('path');
const mysql = require('mysql2/promise');
// 資料庫配置
const dbConfig = {
host: process.env.DB_HOST || 'mysql.theaken.com',
port: parseInt(process.env.DB_PORT || '33306'),
user: process.env.DB_USER || 'AI_Platform',
password: process.env.DB_PASSWORD || 'Aa123456',
database: process.env.DB_NAME || 'db_AI_Platform',
charset: 'utf8mb4',
timezone: '+08:00'
};
async function setupDatabase() {
let connection;
try {
console.log('🚀 開始建立AI展示平台資料庫...');
console.log('─'.repeat(50));
// 1. 連接資料庫
console.log('🔌 連接資料庫...');
connection = await mysql.createConnection({
...dbConfig,
database: undefined // 先不指定資料庫,因為可能不存在
});
// 2. 檢查資料庫是否存在
const [databases] = await connection.query('SHOW DATABASES');
const dbExists = databases.some(db => db.Database === dbConfig.database);
if (!dbExists) {
console.log(`📊 建立資料庫: ${dbConfig.database}`);
await connection.query(`CREATE DATABASE ${dbConfig.database} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci`);
} else {
console.log(`✅ 資料庫已存在: ${dbConfig.database}`);
}
// 3. 切換到目標資料庫
await connection.query(`USE ${dbConfig.database}`);
// 4. 讀取並執行SQL腳本
console.log('📝 執行資料庫建立腳本...');
const sqlScript = fs.readFileSync(path.join(__dirname, '../database_setup_simple.sql'), 'utf8');
// 分割SQL語句並執行
const statements = sqlScript
.split(';')
.map(stmt => stmt.trim())
.filter(stmt => stmt.length > 0 && !stmt.startsWith('--'))
.map(stmt => stmt + ';'); // 重新添加分號
console.log(`📋 找到 ${statements.length} 個SQL語句`);
for (let i = 0; i < statements.length; i++) {
const statement = statements[i];
if (statement.trim() && statement.trim() !== ';') {
try {
console.log(`執行語句 ${i + 1}/${statements.length}: ${statement.substring(0, 50)}...`);
await connection.query(statement);
} catch (error) {
// 忽略一些非關鍵錯誤(如表已存在等)
if (!error.message.includes('already exists') &&
!error.message.includes('Duplicate key') &&
!error.message.includes('Duplicate entry')) {
console.error(`SQL執行錯誤 (語句 ${i + 1}):`, error.message);
console.error('問題語句:', statement.substring(0, 100));
}
}
}
}
console.log('✅ 資料庫建立完成!');
// 5. 驗證建立結果
console.log('\n📋 驗證資料庫結構...');
// 檢查資料表
const [tables] = await connection.query(`
SELECT TABLE_NAME, TABLE_ROWS
FROM information_schema.tables
WHERE table_schema = '${dbConfig.database}'
ORDER BY TABLE_NAME
`);
console.log('\n📊 資料表列表:');
console.log('─'.repeat(60));
console.log('表名'.padEnd(25) + '| 記錄數'.padEnd(10) + '| 狀態');
console.log('─'.repeat(60));
const expectedTables = [
'users', 'competitions', 'judges', 'teams', 'team_members',
'apps', 'proposals', 'judge_scores', 'awards', 'chat_sessions',
'chat_messages', 'ai_assistant_configs', 'user_favorites',
'user_likes', 'competition_participants', 'competition_judges',
'system_settings', 'activity_logs'
];
let successCount = 0;
for (const expectedTable of expectedTables) {
const table = tables.find(t => t.TABLE_NAME === expectedTable);
const status = table ? '✅' : '❌';
const rowCount = table ? (table.TABLE_ROWS || 0) : 'N/A';
console.log(`${expectedTable.padEnd(25)}| ${rowCount.toString().padEnd(10)}| ${status}`);
if (table) successCount++;
}
console.log(`\n📊 成功建立 ${successCount}/${expectedTables.length} 個資料表`);
// 檢查初始數據
console.log('\n📊 初始數據檢查:');
console.log('─'.repeat(40));
const checks = [
{ name: '管理員用戶', query: 'SELECT COUNT(*) as count FROM users WHERE role = "admin"' },
{ name: '預設評審', query: 'SELECT COUNT(*) as count FROM judges' },
{ name: '預設競賽', query: 'SELECT COUNT(*) as count FROM competitions' },
{ name: 'AI配置', query: 'SELECT COUNT(*) as count FROM ai_assistant_configs' },
{ name: '系統設定', query: 'SELECT COUNT(*) as count FROM system_settings' }
];
for (const check of checks) {
try {
const [result] = await connection.query(check.query);
console.log(`${check.name.padEnd(15)}: ${result[0].count}`);
} catch (error) {
console.log(`${check.name.padEnd(15)}: 查詢失敗 - ${error.message}`);
}
}
console.log('\n🎉 資料庫建立和驗證完成!');
console.log('\n📝 下一步:');
console.log('1. 複製 env.example 到 .env.local');
console.log('2. 設定環境變數');
console.log('3. 安裝依賴: pnpm install');
console.log('4. 啟動開發服務器: pnpm dev');
} catch (error) {
console.error('❌ 資料庫建立失敗:', error.message);
console.error('請檢查以下項目:');
console.error('1. 資料庫主機是否可達');
console.error('2. 用戶名和密碼是否正確');
console.error('3. 用戶是否有建立資料庫的權限');
process.exit(1);
} finally {
if (connection) {
await connection.end();
console.log('\n🔌 資料庫連接已關閉');
}
}
}
// 執行建立腳本
if (require.main === module) {
setupDatabase();
}
module.exports = { setupDatabase };

80
scripts/simple-test.js Normal file
View File

@@ -0,0 +1,80 @@
const http = require('http');
// 簡單的 HTTP 請求函數
function makeSimpleRequest(url, method = 'GET', body = null) {
return new Promise((resolve, reject) => {
const urlObj = new URL(url);
const options = {
hostname: urlObj.hostname,
port: urlObj.port,
path: urlObj.pathname,
method: method,
headers: {
'Content-Type': 'application/json'
}
};
const req = http.request(options, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
try {
const jsonData = JSON.parse(data);
resolve({
status: res.statusCode,
data: jsonData
});
} catch (error) {
resolve({
status: res.statusCode,
data: data
});
}
});
});
req.on('error', (error) => {
reject(error);
});
if (body) {
req.write(JSON.stringify(body));
}
req.end();
});
}
async function testSimple() {
console.log('🧪 簡單 API 測試...\n');
try {
// 測試健康檢查
console.log('1⃣ 測試健康檢查...');
const health = await makeSimpleRequest('http://localhost:3000/api');
console.log(` 狀態碼: ${health.status}`);
console.log(` 回應: ${JSON.stringify(health.data, null, 2)}`);
console.log('');
// 測試註冊 API
console.log('2⃣ 測試註冊 API...');
const register = await makeSimpleRequest('http://localhost:3000/api/auth/register', 'POST', {
name: '測試用戶',
email: 'test@example.com',
password: 'Test@2024',
department: '測試部門'
});
console.log(` 狀態碼: ${register.status}`);
console.log(` 回應: ${JSON.stringify(register.data, null, 2)}`);
} catch (error) {
console.error('❌ 測試失敗:', error.message);
}
}
testSimple();

154
scripts/test-api.js Normal file
View File

@@ -0,0 +1,154 @@
const https = require('https');
const http = require('http');
// 測試配置
const BASE_URL = 'http://localhost:3000';
const ADMIN_EMAIL = 'admin@theaken.com';
const ADMIN_PASSWORD = 'Admin@2024';
// 測試用戶資料
const TEST_USER = {
name: '測試用戶',
email: 'test@theaken.com',
password: 'Test@2024',
department: '測試部門',
role: 'user'
};
// 發送 HTTP 請求
async function makeRequest(url, options = {}) {
return new Promise((resolve, reject) => {
const urlObj = new URL(url);
const isHttps = urlObj.protocol === 'https:';
const client = isHttps ? https : http;
const requestOptions = {
hostname: urlObj.hostname,
port: urlObj.port,
path: urlObj.pathname + urlObj.search,
method: options.method || 'GET',
headers: {
'Content-Type': 'application/json',
...options.headers
}
};
const req = client.request(requestOptions, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
try {
const jsonData = JSON.parse(data);
resolve({
status: res.statusCode,
headers: res.headers,
data: jsonData
});
} catch (error) {
resolve({
status: res.statusCode,
headers: res.headers,
data: data
});
}
});
});
req.on('error', (error) => {
reject(error);
});
if (options.body) {
req.write(JSON.stringify(options.body));
}
req.end();
});
}
// 測試函數
async function testAPI() {
console.log('🧪 開始測試 API...\n');
try {
// 1. 測試健康檢查
console.log('1⃣ 測試健康檢查 API...');
const healthResponse = await makeRequest(`${BASE_URL}/api`);
console.log(` 狀態碼: ${healthResponse.status}`);
console.log(` 回應: ${JSON.stringify(healthResponse.data, null, 2)}`);
console.log('');
// 2. 測試註冊 API
console.log('2⃣ 測試註冊 API...');
const registerResponse = await makeRequest(`${BASE_URL}/api/auth/register`, {
method: 'POST',
body: TEST_USER
});
console.log(` 狀態碼: ${registerResponse.status}`);
console.log(` 回應: ${JSON.stringify(registerResponse.data, null, 2)}`);
console.log('');
// 3. 測試登入 API
console.log('3⃣ 測試登入 API...');
const loginResponse = await makeRequest(`${BASE_URL}/api/auth/login`, {
method: 'POST',
body: {
email: ADMIN_EMAIL,
password: ADMIN_PASSWORD
}
});
console.log(` 狀態碼: ${loginResponse.status}`);
let authToken = null;
if (loginResponse.status === 200) {
authToken = loginResponse.data.token;
console.log(` 登入成功,獲得 Token`);
} else {
console.log(` 登入失敗: ${JSON.stringify(loginResponse.data, null, 2)}`);
}
console.log('');
// 4. 測試獲取當前用戶 API
if (authToken) {
console.log('4⃣ 測試獲取當前用戶 API...');
const meResponse = await makeRequest(`${BASE_URL}/api/auth/me`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
console.log(` 狀態碼: ${meResponse.status}`);
console.log(` 回應: ${JSON.stringify(meResponse.data, null, 2)}`);
console.log('');
}
// 5. 測試用戶列表 API (需要管理員權限)
if (authToken) {
console.log('5⃣ 測試用戶列表 API...');
const usersResponse = await makeRequest(`${BASE_URL}/api/users`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
console.log(` 狀態碼: ${usersResponse.status}`);
console.log(` 回應: ${JSON.stringify(usersResponse.data, null, 2)}`);
console.log('');
}
console.log('✅ API 測試完成!');
} catch (error) {
console.error('❌ API 測試失敗:', error.message);
console.error('錯誤詳情:', error);
}
}
// 執行測試
if (require.main === module) {
testAPI();
}
module.exports = { testAPI };