新增資料庫、用戶註冊、登入的功能
This commit is contained in:
109
BACKEND_STAGE1_REPORT.md
Normal file
109
BACKEND_STAGE1_REPORT.md
Normal 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。
|
@@ -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
379
DATABASE_GUIDE.md
Normal 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
186
DATABASE_SETUP_COMPLETE.md
Normal 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展示平台開發團隊
|
||||||
|
**狀態**: ✅ 完成
|
@@ -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
73
SECURITY_CHECKLIST.md
Normal 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 金鑰
|
||||||
|
- 刪除測試頁面
|
||||||
|
- 更新文檔
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**注意**: 此清單應定期更新,確保系統安全性。
|
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
@@ -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
115
app/api/auth/login/route.ts
Normal 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
47
app/api/auth/me/route.ts
Normal 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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
113
app/api/auth/register/route.ts
Normal file
113
app/api/auth/register/route.ts
Normal 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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
19
app/api/auth/reset-password/confirm/route.ts
Normal file
19
app/api/auth/reset-password/confirm/route.ts
Normal 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 });
|
||||||
|
}
|
||||||
|
}
|
20
app/api/auth/reset-password/request/route.ts
Normal file
20
app/api/auth/reset-password/request/route.ts
Normal 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
35
app/api/route.ts
Normal 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
47
app/api/users/route.ts
Normal 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 });
|
||||||
|
}
|
||||||
|
}
|
23
app/api/users/stats/route.ts
Normal file
23
app/api/users/stats/route.ts
Normal 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 });
|
||||||
|
}
|
||||||
|
}
|
@@ -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>
|
||||||
|
@@ -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">
|
||||||
|
@@ -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) => {
|
||||||
|
@@ -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>
|
||||||
|
|
||||||
|
@@ -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 ? "登入中..." : "登入"}
|
||||||
|
@@ -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">
|
||||||
|
@@ -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助手,專門幫助用戶了解如何使用這個系統。
|
||||||
|
@@ -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 = () => {
|
||||||
|
77
database_connection_test.js
Normal file
77
database_connection_test.js
Normal 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
511
database_setup.sql
Normal 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
344
database_setup_simple.sql
Normal 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
36
env.example
Normal 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
209
lib/auth.ts
Normal 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
213
lib/database.ts
Normal 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
163
lib/logger.ts
Normal 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);
|
||||||
|
}
|
12
package.json
12
package.json
@@ -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
248
pnpm-lock.yaml
generated
@@ -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
2
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ignoredBuiltDependencies:
|
||||||
|
- bcrypt
|
134
scripts/create-admin.js
Normal file
134
scripts/create-admin.js
Normal 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
136
scripts/fix-tables.js
Normal 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
80
scripts/fix-user-likes.js
Normal 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 };
|
491
scripts/setup-database-manual.js
Normal file
491
scripts/setup-database-manual.js
Normal 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
160
scripts/setup-database.js
Normal 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
80
scripts/simple-test.js
Normal 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
154
scripts/test-api.js
Normal 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 };
|
Reference in New Issue
Block a user