完成品
This commit is contained in:
232
README.md
Normal file
232
README.md
Normal file
@@ -0,0 +1,232 @@
|
||||
# AI 展示平台 (AI Showcase Platform)
|
||||
|
||||
一個現代化的 AI 應用展示和管理平台,支持競賽管理、評分系統和用戶互動功能。
|
||||
|
||||
## 🚀 功能特色
|
||||
|
||||
### 📱 應用管理
|
||||
- **應用展示**: 展示所有 AI 應用,支持分類和搜索
|
||||
- **分頁功能**: 每頁顯示5個應用,支持翻頁瀏覽
|
||||
- **統計數據**: 實時顯示總應用數、已發布數、已下架數
|
||||
- **應用詳情**: 查看應用的詳細信息、統計數據和用戶評價
|
||||
|
||||
### 🏆 競賽系統
|
||||
- **競賽管理**: 創建和管理不同類型的競賽(個人、團隊、混合)
|
||||
- **評審系統**: 支持多評審評分,包含創新性、技術性、實用性等維度
|
||||
- **評分管理**: 實時追蹤評分進度和統計數據
|
||||
- **排名系統**: 自動計算和顯示競賽排名
|
||||
|
||||
### 👥 用戶管理
|
||||
- **用戶註冊**: 支持用戶註冊和登錄
|
||||
- **權限管理**: 區分普通用戶、開發者和管理員權限
|
||||
- **個人資料**: 用戶可以管理個人信息和偏好設置
|
||||
|
||||
### 📊 數據分析
|
||||
- **統計儀表板**: 實時顯示平台使用統計
|
||||
- **應用分析**: 查看應用的瀏覽量、點讚數、評分等數據
|
||||
- **競賽分析**: 競賽參與度和評分統計
|
||||
|
||||
## 🛠️ 技術棧
|
||||
|
||||
### 前端
|
||||
- **框架**: Next.js 14 (React 18)
|
||||
- **樣式**: Tailwind CSS
|
||||
- **UI 組件**: shadcn/ui
|
||||
- **狀態管理**: React Context API
|
||||
- **圖標**: Lucide React
|
||||
|
||||
### 後端
|
||||
- **API**: Next.js API Routes
|
||||
- **數據庫**: MySQL 8.0
|
||||
- **ORM**: 自定義數據庫服務層
|
||||
- **認證**: 自定義 JWT 認證系統
|
||||
|
||||
### 開發工具
|
||||
- **包管理**: pnpm
|
||||
- **代碼格式化**: Prettier
|
||||
- **類型檢查**: TypeScript
|
||||
- **數據庫遷移**: 自定義遷移腳本
|
||||
|
||||
## 📦 安裝與設置
|
||||
|
||||
### 環境要求
|
||||
- Node.js 18.0.0 或更高版本
|
||||
- pnpm (推薦) 或 npm
|
||||
- MySQL 8.0 或更高版本
|
||||
|
||||
### 1. 克隆項目
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd ai-showcase-platform
|
||||
```
|
||||
|
||||
### 2. 安裝依賴
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
### 3. 環境配置
|
||||
複製環境變數範例文件:
|
||||
```bash
|
||||
cp env.example .env.local
|
||||
```
|
||||
|
||||
編輯 `.env.local` 文件,填入您的數據庫配置:
|
||||
```env
|
||||
DB_HOST=your-database-host
|
||||
DB_PORT=3306
|
||||
DB_USER=your-username
|
||||
DB_PASSWORD=your-password
|
||||
DB_NAME=your-database-name
|
||||
```
|
||||
|
||||
### 4. 數據庫設置
|
||||
運行數據庫遷移:
|
||||
```bash
|
||||
# 創建數據庫結構
|
||||
node scripts/migrate-no-triggers.js
|
||||
|
||||
# 填充示例數據
|
||||
node scripts/populate-sample-data.js
|
||||
```
|
||||
|
||||
### 5. 啟動開發服務器
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
訪問 http://localhost:3000 查看應用。
|
||||
|
||||
## 📁 項目結構
|
||||
|
||||
```
|
||||
ai-showcase-platform/
|
||||
├── app/ # Next.js App Router
|
||||
│ ├── admin/ # 管理員頁面
|
||||
│ ├── api/ # API 路由
|
||||
│ │ ├── admin/ # 管理員 API
|
||||
│ │ ├── auth/ # 認證 API
|
||||
│ │ ├── competitions/ # 競賽 API
|
||||
│ │ └── user/ # 用戶 API
|
||||
│ ├── competition/ # 競賽頁面
|
||||
│ └── page.tsx # 首頁
|
||||
├── components/ # React 組件
|
||||
│ ├── admin/ # 管理員組件
|
||||
│ ├── auth/ # 認證組件
|
||||
│ ├── competition/ # 競賽組件
|
||||
│ └── ui/ # UI 組件庫
|
||||
├── contexts/ # React Context
|
||||
├── hooks/ # 自定義 Hooks
|
||||
├── lib/ # 工具庫
|
||||
│ ├── services/ # 數據庫服務
|
||||
│ └── utils.ts # 工具函數
|
||||
├── scripts/ # 腳本文件
|
||||
│ ├── migrate-no-triggers.js # 數據庫遷移
|
||||
│ ├── populate-sample-data.js # 示例數據
|
||||
│ └── check-*.js # 檢查腳本
|
||||
├── types/ # TypeScript 類型定義
|
||||
└── public/ # 靜態資源
|
||||
```
|
||||
|
||||
## 🔧 開發指南
|
||||
|
||||
### 數據庫管理
|
||||
```bash
|
||||
# 檢查數據庫連接
|
||||
node scripts/check-server.js
|
||||
|
||||
# 檢查應用數據
|
||||
node scripts/check-apps-count.js
|
||||
|
||||
# 清空數據庫
|
||||
node scripts/clear-database.js
|
||||
|
||||
# 重新填充數據
|
||||
node scripts/populate-sample-data.js
|
||||
```
|
||||
|
||||
### API 測試
|
||||
```bash
|
||||
# 測試應用 API
|
||||
node scripts/test-pagination-api.js
|
||||
|
||||
# 測試分頁功能
|
||||
node scripts/test-pagination.js
|
||||
```
|
||||
|
||||
### 代碼規範
|
||||
- 使用 TypeScript 進行類型檢查
|
||||
- 遵循 ESLint 規則
|
||||
- 使用 Prettier 格式化代碼
|
||||
- 組件使用函數式組件和 Hooks
|
||||
|
||||
## 📊 數據庫設計
|
||||
|
||||
### 主要表結構
|
||||
- **users**: 用戶信息
|
||||
- **apps**: 應用信息
|
||||
- **competitions**: 競賽信息
|
||||
- **judges**: 評審信息
|
||||
- **app_judge_scores**: 應用評分
|
||||
- **competition_apps**: 競賽應用關聯
|
||||
- **competition_judges**: 競賽評審關聯
|
||||
|
||||
### 關係設計
|
||||
- 用戶與應用:一對多
|
||||
- 競賽與應用:多對多
|
||||
- 競賽與評審:多對多
|
||||
- 評審與評分:一對多
|
||||
|
||||
## 🚀 部署
|
||||
|
||||
### 生產環境設置
|
||||
1. 設置生產環境變數
|
||||
2. 配置數據庫連接
|
||||
3. 運行數據庫遷移
|
||||
4. 構建應用:`pnpm build`
|
||||
5. 啟動應用:`pnpm start`
|
||||
|
||||
### Docker 部署(可選)
|
||||
```dockerfile
|
||||
FROM node:18-alpine
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
EXPOSE 3000
|
||||
CMD ["npm", "start"]
|
||||
```
|
||||
|
||||
## 🤝 貢獻指南
|
||||
|
||||
1. Fork 項目
|
||||
2. 創建功能分支:`git checkout -b feature/AmazingFeature`
|
||||
3. 提交更改:`git commit -m 'Add some AmazingFeature'`
|
||||
4. 推送到分支:`git push origin feature/AmazingFeature`
|
||||
5. 開啟 Pull Request
|
||||
|
||||
## 📝 更新日誌
|
||||
|
||||
### v1.0.0 (2024-09-19)
|
||||
- ✨ 初始版本發布
|
||||
- 🎯 應用管理功能
|
||||
- 🏆 競賽系統
|
||||
- 👥 用戶管理
|
||||
- 📊 統計儀表板
|
||||
- 🔧 分頁功能優化
|
||||
|
||||
## 📄 許可證
|
||||
|
||||
此項目採用 MIT 許可證 - 查看 [LICENSE](LICENSE) 文件了解詳情。
|
||||
|
||||
## 📞 聯繫方式
|
||||
|
||||
如有問題或建議,請聯繫:
|
||||
- 項目維護者:[您的姓名]
|
||||
- 郵箱:[your-email@example.com]
|
||||
- 項目地址:[repository-url]
|
||||
|
||||
---
|
||||
|
||||
**注意**: 這是一個內部使用的 AI 展示平台,請確保在生產環境中進行適當的安全配置。
|
@@ -7,7 +7,7 @@ export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const page = parseInt(searchParams.get('page') || '1')
|
||||
const limit = parseInt(searchParams.get('limit') || '10')
|
||||
const limit = parseInt(searchParams.get('limit') || '5')
|
||||
const search = searchParams.get('search') || ''
|
||||
const category = searchParams.get('category') || 'all'
|
||||
const type = searchParams.get('type') || 'all'
|
||||
|
@@ -165,7 +165,7 @@ export function AppManagement() {
|
||||
})
|
||||
const [pagination, setPagination] = useState({
|
||||
page: 1,
|
||||
limit: 10,
|
||||
limit: 5,
|
||||
total: 0,
|
||||
totalPages: 0
|
||||
})
|
||||
@@ -608,7 +608,7 @@ export function AppManagement() {
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">總應用數</p>
|
||||
<p className="text-2xl font-bold">{apps.length}</p>
|
||||
<p className="text-2xl font-bold">{stats.totalApps}</p>
|
||||
</div>
|
||||
<Bot className="w-8 h-8 text-blue-600" />
|
||||
</div>
|
||||
@@ -620,7 +620,7 @@ export function AppManagement() {
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">已發布</p>
|
||||
<p className="text-2xl font-bold">{apps.filter((a) => a.status === "published").length}</p>
|
||||
<p className="text-2xl font-bold">{stats.activeApps}</p>
|
||||
</div>
|
||||
<CheckCircle className="w-8 h-8 text-green-600" />
|
||||
</div>
|
||||
@@ -632,7 +632,7 @@ export function AppManagement() {
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">已下架</p>
|
||||
<p className="text-2xl font-bold">{apps.filter((a) => a.status === "draft").length}</p>
|
||||
<p className="text-2xl font-bold">{stats.inactiveApps}</p>
|
||||
</div>
|
||||
<XCircle className="w-8 h-8 text-red-600" />
|
||||
</div>
|
||||
@@ -695,7 +695,7 @@ export function AppManagement() {
|
||||
{/* Apps Table */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>應用列表 ({filteredApps.length})</CardTitle>
|
||||
<CardTitle>應用列表 ({pagination.total})</CardTitle>
|
||||
<CardDescription>管理所有 AI 應用</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -842,6 +842,65 @@ export function AppManagement() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 分頁控制元件 */}
|
||||
{pagination.totalPages > 1 && (
|
||||
<div className="flex flex-col items-center space-y-4 mt-6">
|
||||
<div className="text-sm text-gray-600">
|
||||
第 {pagination.page} 頁,共 {pagination.totalPages} 頁 (共 {pagination.total} 個應用)
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setPagination(prev => ({ ...prev, page: Math.max(1, prev.page - 1) }))}
|
||||
disabled={pagination.page === 1}
|
||||
className="flex items-center space-x-1"
|
||||
>
|
||||
<span>‹</span>
|
||||
<span>上一頁</span>
|
||||
</Button>
|
||||
|
||||
<div className="flex items-center space-x-1">
|
||||
{Array.from({ length: Math.min(pagination.totalPages, 5) }, (_, i) => {
|
||||
let page;
|
||||
if (pagination.totalPages <= 5) {
|
||||
page = i + 1;
|
||||
} else if (pagination.page <= 3) {
|
||||
page = i + 1;
|
||||
} else if (pagination.page >= pagination.totalPages - 2) {
|
||||
page = pagination.totalPages - 4 + i;
|
||||
} else {
|
||||
page = pagination.page - 2 + i;
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={page}
|
||||
variant={pagination.page === page ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => setPagination(prev => ({ ...prev, page }))}
|
||||
className="w-10 h-10 p-0"
|
||||
>
|
||||
{page}
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setPagination(prev => ({ ...prev, page: Math.min(prev.totalPages, prev.page + 1) }))}
|
||||
disabled={pagination.page === pagination.totalPages}
|
||||
className="flex items-center space-x-1"
|
||||
>
|
||||
<span>下一頁</span>
|
||||
<span>›</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Add App Dialog */}
|
||||
<Dialog open={showAddApp} onOpenChange={setShowAddApp}>
|
||||
<DialogContent className="max-w-4xl max-h-[85vh] overflow-y-auto">
|
||||
|
@@ -475,7 +475,7 @@ BEGIN
|
||||
NEW.presentation_score +
|
||||
NEW.impact_score
|
||||
) / 5.0;
|
||||
END//
|
||||
END;
|
||||
|
||||
CREATE TRIGGER `calculate_app_total_score_update`
|
||||
BEFORE UPDATE ON `app_judge_scores`
|
||||
@@ -488,7 +488,7 @@ BEGIN
|
||||
NEW.presentation_score +
|
||||
NEW.impact_score
|
||||
) / 5.0;
|
||||
END//
|
||||
END;
|
||||
|
||||
CREATE TRIGGER `calculate_proposal_total_score`
|
||||
BEFORE INSERT ON `proposal_judge_scores`
|
||||
@@ -501,7 +501,7 @@ BEGIN
|
||||
NEW.impact_score +
|
||||
NEW.presentation_score
|
||||
) / 5.0;
|
||||
END//
|
||||
END;
|
||||
|
||||
CREATE TRIGGER `calculate_proposal_total_score_update`
|
||||
BEFORE UPDATE ON `proposal_judge_scores`
|
||||
@@ -514,9 +514,7 @@ BEGIN
|
||||
NEW.impact_score +
|
||||
NEW.presentation_score
|
||||
) / 5.0;
|
||||
END//
|
||||
|
||||
DELIMITER ;
|
||||
END;
|
||||
|
||||
-- 視圖:用戶統計視圖
|
||||
CREATE VIEW `user_statistics` AS
|
||||
|
@@ -1976,7 +1976,7 @@ export class AppService {
|
||||
limit?: number;
|
||||
} = {}): Promise<{ apps: any[]; total: number }> {
|
||||
try {
|
||||
const { search = '', category = 'all', type = 'all', status = 'all', page = 1, limit = 10 } = filters;
|
||||
const { search = '', category = 'all', type = 'all', status = 'all', page = 1, limit = 5 } = filters;
|
||||
|
||||
// 構建查詢條件
|
||||
let whereConditions: string[] = [];
|
||||
@@ -2846,11 +2846,12 @@ export class AppService {
|
||||
const appStatsSql = `
|
||||
SELECT
|
||||
COUNT(*) as total_apps,
|
||||
COUNT(CASE WHEN is_active = TRUE THEN 1 END) as active_apps,
|
||||
COUNT(CASE WHEN is_active = FALSE THEN 1 END) as inactive_apps,
|
||||
COUNT(CASE WHEN created_at >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) THEN 1 END) as new_apps_this_month,
|
||||
COALESCE(SUM(views_count), 0) as total_views,
|
||||
COALESCE(SUM(likes_count), 0) as total_likes
|
||||
FROM apps
|
||||
WHERE is_active = TRUE
|
||||
`;
|
||||
const appStats = await this.queryOne(appStatsSql);
|
||||
|
||||
@@ -2888,6 +2889,8 @@ export class AppService {
|
||||
totalUsers: userStats.totalUsers,
|
||||
activeUsers: userStats.activeUsers,
|
||||
totalApps: appStats?.total_apps || 0,
|
||||
activeApps: appStats?.active_apps || 0,
|
||||
inactiveApps: appStats?.inactive_apps || 0,
|
||||
totalCompetitions: competitionStats?.total_competitions || 0,
|
||||
totalReviews: reviewStats?.total_reviews || 0,
|
||||
totalViews: appStats?.total_views || 0,
|
||||
|
@@ -4,7 +4,7 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "next build",
|
||||
"dev": "next dev",
|
||||
"dev": "next dev -p 12016",
|
||||
"lint": "next lint",
|
||||
"start": "next start",
|
||||
"migrate": "node scripts/migrate.js",
|
||||
|
34
scripts/clear-database.js
Normal file
34
scripts/clear-database.js
Normal file
@@ -0,0 +1,34 @@
|
||||
const mysql = require('mysql2/promise');
|
||||
|
||||
async function clearDatabase() {
|
||||
const connection = await mysql.createConnection({
|
||||
host: 'mysql.theaken.com',
|
||||
port: 33306,
|
||||
user: 'AI_Platform',
|
||||
password: 'Aa123456',
|
||||
database: 'db_AI_Platform'
|
||||
});
|
||||
|
||||
console.log('🗑️ 清空資料庫...');
|
||||
|
||||
// 清空所有表(按依賴順序)
|
||||
const tables = [
|
||||
'app_judge_scores',
|
||||
'competition_apps',
|
||||
'competition_judges',
|
||||
'apps',
|
||||
'judges',
|
||||
'competitions',
|
||||
'users'
|
||||
];
|
||||
|
||||
for (const table of tables) {
|
||||
await connection.execute(`DELETE FROM ${table}`);
|
||||
console.log(`✅ 清空了 ${table} 表`);
|
||||
}
|
||||
|
||||
await connection.end();
|
||||
console.log('🎉 資料庫清空完成!');
|
||||
}
|
||||
|
||||
clearDatabase().catch(console.error);
|
141
scripts/migrate-fixed.js
Normal file
141
scripts/migrate-fixed.js
Normal file
@@ -0,0 +1,141 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// =====================================================
|
||||
// 資料庫遷移腳本 (修復版)
|
||||
// =====================================================
|
||||
|
||||
const mysql = require('mysql2/promise');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// 資料庫配置
|
||||
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 runMigration() {
|
||||
let connection;
|
||||
|
||||
try {
|
||||
console.log('🚀 開始資料庫遷移...');
|
||||
|
||||
// 創建連接
|
||||
connection = await mysql.createConnection({
|
||||
...dbConfig,
|
||||
multipleStatements: true
|
||||
});
|
||||
|
||||
console.log('✅ 資料庫連接成功');
|
||||
|
||||
// 讀取 SQL 文件
|
||||
const sqlFile = path.join(__dirname, '..', 'database-schema-simple.sql');
|
||||
const sqlContent = fs.readFileSync(sqlFile, 'utf8');
|
||||
|
||||
console.log('📖 讀取 SQL 文件成功');
|
||||
|
||||
// 處理 SQL 內容,正確分割語句
|
||||
console.log('⚡ 處理 SQL 語句...');
|
||||
|
||||
// 先處理觸發器,因為它們包含分號
|
||||
const triggerRegex = /CREATE TRIGGER[\s\S]*?END;/g;
|
||||
const triggers = sqlContent.match(triggerRegex) || [];
|
||||
|
||||
// 移除觸發器部分,處理其他語句
|
||||
let remainingSql = sqlContent.replace(triggerRegex, '');
|
||||
|
||||
// 分割其他語句
|
||||
const otherStatements = remainingSql
|
||||
.split(';')
|
||||
.map(stmt => stmt.trim())
|
||||
.filter(stmt => stmt.length > 0 && !stmt.startsWith('--'));
|
||||
|
||||
// 合併所有語句
|
||||
const allStatements = [...otherStatements, ...triggers];
|
||||
|
||||
console.log(`📊 共找到 ${allStatements.length} 個語句`);
|
||||
|
||||
// 執行語句
|
||||
for (let i = 0; i < allStatements.length; i++) {
|
||||
const statement = allStatements[i];
|
||||
if (statement.trim()) {
|
||||
try {
|
||||
// 特殊處理 USE 語句和觸發器
|
||||
if (statement.toUpperCase().startsWith('USE') ||
|
||||
statement.toUpperCase().startsWith('CREATE TRIGGER')) {
|
||||
await connection.query(statement + ';');
|
||||
} else {
|
||||
await connection.execute(statement + ';');
|
||||
}
|
||||
console.log(`✅ 執行語句 ${i + 1}/${allStatements.length}`);
|
||||
} catch (error) {
|
||||
console.error(`❌ 語句 ${i + 1} 執行失敗:`, error.message);
|
||||
console.error(`語句內容: ${statement.substring(0, 100)}...`);
|
||||
// 對於某些錯誤,我們可以繼續執行
|
||||
if (error.message.includes('already exists') ||
|
||||
error.message.includes('Duplicate entry') ||
|
||||
error.message.includes('Table') && error.message.includes('already exists')) {
|
||||
console.log('⚠️ 跳過已存在的項目');
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('✅ 資料庫結構創建成功!');
|
||||
|
||||
// 驗證表是否創建成功
|
||||
console.log('🔍 驗證表結構...');
|
||||
const [tables] = await connection.execute('SHOW TABLES');
|
||||
console.log(`📊 共創建了 ${tables.length} 個表:`);
|
||||
|
||||
tables.forEach((table, index) => {
|
||||
const tableName = Object.values(table)[0];
|
||||
console.log(` ${index + 1}. ${tableName}`);
|
||||
});
|
||||
|
||||
// 檢查視圖
|
||||
console.log('🔍 驗證視圖...');
|
||||
const [views] = await connection.execute('SHOW FULL TABLES WHERE Table_type = "VIEW"');
|
||||
console.log(`📈 共創建了 ${views.length} 個視圖:`);
|
||||
|
||||
views.forEach((view, index) => {
|
||||
const viewName = Object.values(view)[0];
|
||||
console.log(` ${index + 1}. ${viewName}`);
|
||||
});
|
||||
|
||||
// 檢查觸發器
|
||||
console.log('🔍 驗證觸發器...');
|
||||
const [triggerList] = await connection.execute('SHOW TRIGGERS');
|
||||
console.log(`⚙️ 共創建了 ${triggerList.length} 個觸發器:`);
|
||||
|
||||
triggerList.forEach((trigger, index) => {
|
||||
console.log(` ${index + 1}. ${trigger.Trigger}`);
|
||||
});
|
||||
|
||||
console.log('🎉 資料庫遷移完成!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 遷移失敗:', error.message);
|
||||
console.error('詳細錯誤:', error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
if (connection) {
|
||||
await connection.end();
|
||||
console.log('🔌 資料庫連接已關閉');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 執行遷移
|
||||
if (require.main === module) {
|
||||
runMigration().catch(console.error);
|
||||
}
|
||||
|
||||
module.exports = { runMigration };
|
124
scripts/migrate-no-triggers.js
Normal file
124
scripts/migrate-no-triggers.js
Normal file
@@ -0,0 +1,124 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// =====================================================
|
||||
// 資料庫遷移腳本 (無觸發器版)
|
||||
// =====================================================
|
||||
|
||||
const mysql = require('mysql2/promise');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// 資料庫配置
|
||||
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 runMigration() {
|
||||
let connection;
|
||||
|
||||
try {
|
||||
console.log('🚀 開始資料庫遷移...');
|
||||
|
||||
// 創建連接
|
||||
connection = await mysql.createConnection({
|
||||
...dbConfig,
|
||||
multipleStatements: true
|
||||
});
|
||||
|
||||
console.log('✅ 資料庫連接成功');
|
||||
|
||||
// 讀取 SQL 文件
|
||||
const sqlFile = path.join(__dirname, '..', 'database-schema-simple.sql');
|
||||
const sqlContent = fs.readFileSync(sqlFile, 'utf8');
|
||||
|
||||
console.log('📖 讀取 SQL 文件成功');
|
||||
|
||||
// 移除觸發器部分
|
||||
console.log('⚡ 處理 SQL 語句...');
|
||||
const triggerRegex = /CREATE TRIGGER[\s\S]*?END;/g;
|
||||
let sqlWithoutTriggers = sqlContent.replace(triggerRegex, '');
|
||||
|
||||
// 分割語句
|
||||
const statements = sqlWithoutTriggers
|
||||
.split(';')
|
||||
.map(stmt => stmt.trim())
|
||||
.filter(stmt => stmt.length > 0 && !stmt.startsWith('--'));
|
||||
|
||||
console.log(`📊 共找到 ${statements.length} 個語句`);
|
||||
|
||||
// 執行語句
|
||||
for (let i = 0; i < statements.length; i++) {
|
||||
const statement = statements[i];
|
||||
if (statement.trim()) {
|
||||
try {
|
||||
// 特殊處理 USE 語句
|
||||
if (statement.toUpperCase().startsWith('USE')) {
|
||||
await connection.query(statement + ';');
|
||||
} else {
|
||||
await connection.execute(statement + ';');
|
||||
}
|
||||
console.log(`✅ 執行語句 ${i + 1}/${statements.length}`);
|
||||
} catch (error) {
|
||||
console.error(`❌ 語句 ${i + 1} 執行失敗:`, error.message);
|
||||
console.error(`語句內容: ${statement.substring(0, 100)}...`);
|
||||
// 對於某些錯誤,我們可以繼續執行
|
||||
if (error.message.includes('already exists') ||
|
||||
error.message.includes('Duplicate entry') ||
|
||||
error.message.includes('Table') && error.message.includes('already exists')) {
|
||||
console.log('⚠️ 跳過已存在的項目');
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('✅ 資料庫結構創建成功!');
|
||||
|
||||
// 驗證表是否創建成功
|
||||
console.log('🔍 驗證表結構...');
|
||||
const [tables] = await connection.execute('SHOW TABLES');
|
||||
console.log(`📊 共創建了 ${tables.length} 個表:`);
|
||||
|
||||
tables.forEach((table, index) => {
|
||||
const tableName = Object.values(table)[0];
|
||||
console.log(` ${index + 1}. ${tableName}`);
|
||||
});
|
||||
|
||||
// 檢查視圖
|
||||
console.log('🔍 驗證視圖...');
|
||||
const [views] = await connection.execute('SHOW FULL TABLES WHERE Table_type = "VIEW"');
|
||||
console.log(`📈 共創建了 ${views.length} 個視圖:`);
|
||||
|
||||
views.forEach((view, index) => {
|
||||
const viewName = Object.values(view)[0];
|
||||
console.log(` ${index + 1}. ${viewName}`);
|
||||
});
|
||||
|
||||
console.log('🎉 資料庫遷移完成!');
|
||||
console.log('⚠️ 注意:觸發器由於權限限制未創建,但基本表結構已就緒');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 遷移失敗:', error.message);
|
||||
console.error('詳細錯誤:', error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
if (connection) {
|
||||
await connection.end();
|
||||
console.log('🔌 資料庫連接已關閉');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 執行遷移
|
||||
if (require.main === module) {
|
||||
runMigration().catch(console.error);
|
||||
}
|
||||
|
||||
module.exports = { runMigration };
|
319
scripts/populate-sample-data.js
Normal file
319
scripts/populate-sample-data.js
Normal file
@@ -0,0 +1,319 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// =====================================================
|
||||
// 填充示例數據腳本
|
||||
// =====================================================
|
||||
|
||||
const mysql = require('mysql2/promise');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
|
||||
// 資料庫配置
|
||||
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 populateSampleData() {
|
||||
let connection;
|
||||
|
||||
try {
|
||||
console.log('🚀 開始填充示例數據...');
|
||||
|
||||
// 創建連接
|
||||
connection = await mysql.createConnection(dbConfig);
|
||||
console.log('✅ 資料庫連接成功');
|
||||
|
||||
// 1. 創建示例用戶
|
||||
console.log('👥 創建示例用戶...');
|
||||
const users = [
|
||||
{
|
||||
id: uuidv4(),
|
||||
name: '張小明',
|
||||
email: 'zhang.xiaoming@company.com',
|
||||
password_hash: '$2b$10$example.hash.here', // 示例哈希
|
||||
department: 'HQBU',
|
||||
role: 'developer',
|
||||
join_date: '2024-01-15',
|
||||
total_likes: 25,
|
||||
total_views: 150,
|
||||
is_active: true
|
||||
},
|
||||
{
|
||||
id: uuidv4(),
|
||||
name: '李美華',
|
||||
email: 'li.meihua@company.com',
|
||||
password_hash: '$2b$10$example.hash.here',
|
||||
department: 'ITBU',
|
||||
role: 'developer',
|
||||
join_date: '2024-02-01',
|
||||
total_likes: 18,
|
||||
total_views: 120,
|
||||
is_active: true
|
||||
},
|
||||
{
|
||||
id: uuidv4(),
|
||||
name: '王大偉',
|
||||
email: 'wang.dawei@company.com',
|
||||
password_hash: '$2b$10$example.hash.here',
|
||||
department: 'MBU1',
|
||||
role: 'developer',
|
||||
join_date: '2024-01-20',
|
||||
total_likes: 32,
|
||||
total_views: 200,
|
||||
is_active: true
|
||||
},
|
||||
{
|
||||
id: uuidv4(),
|
||||
name: '陳小芳',
|
||||
email: 'chen.xiaofang@company.com',
|
||||
password_hash: '$2b$10$example.hash.here',
|
||||
department: 'SBU',
|
||||
role: 'developer',
|
||||
join_date: '2024-02-10',
|
||||
total_likes: 15,
|
||||
total_views: 90,
|
||||
is_active: true
|
||||
},
|
||||
{
|
||||
id: uuidv4(),
|
||||
name: '劉志強',
|
||||
email: 'liu.zhiqiang@company.com',
|
||||
password_hash: '$2b$10$example.hash.here',
|
||||
department: 'HQBU',
|
||||
role: 'admin',
|
||||
join_date: '2023-12-01',
|
||||
total_likes: 5,
|
||||
total_views: 50,
|
||||
is_active: true
|
||||
}
|
||||
];
|
||||
|
||||
for (const user of users) {
|
||||
await connection.execute(
|
||||
`INSERT INTO users (id, name, email, password_hash, department, role, join_date, total_likes, total_views, status, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())`,
|
||||
[user.id, user.name, user.email, user.password_hash, user.department, user.role, user.join_date, user.total_likes, user.total_views, 'active']
|
||||
);
|
||||
}
|
||||
console.log(`✅ 創建了 ${users.length} 個用戶`);
|
||||
|
||||
// 2. 創建示例評審
|
||||
console.log('👨⚖️ 創建示例評審...');
|
||||
const judges = [
|
||||
{
|
||||
id: uuidv4(),
|
||||
name: '王教授',
|
||||
title: '技術總監',
|
||||
department: 'ITBU',
|
||||
expertise: JSON.stringify(['AI', '機器學習', '深度學習']),
|
||||
is_active: true
|
||||
},
|
||||
{
|
||||
id: uuidv4(),
|
||||
name: '李博士',
|
||||
title: '產品經理',
|
||||
department: 'HQBU',
|
||||
expertise: JSON.stringify(['產品設計', '用戶體驗', '商業分析']),
|
||||
is_active: true
|
||||
},
|
||||
{
|
||||
id: uuidv4(),
|
||||
name: '陳工程師',
|
||||
title: '資深工程師',
|
||||
department: 'MBU1',
|
||||
expertise: JSON.stringify(['軟體開發', '系統架構', '資料庫']),
|
||||
is_active: true
|
||||
}
|
||||
];
|
||||
|
||||
for (const judge of judges) {
|
||||
await connection.execute(
|
||||
`INSERT INTO judges (id, name, title, department, expertise, is_active, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, NOW(), NOW())`,
|
||||
[judge.id, judge.name, judge.title, judge.department, judge.expertise, judge.is_active]
|
||||
);
|
||||
}
|
||||
console.log(`✅ 創建了 ${judges.length} 個評審`);
|
||||
|
||||
// 3. 創建示例競賽
|
||||
console.log('🏆 創建示例競賽...');
|
||||
const competitions = [
|
||||
{
|
||||
id: uuidv4(),
|
||||
name: '2024年AI創新競賽',
|
||||
description: '展示最新的AI技術創新成果',
|
||||
type: 'individual',
|
||||
year: 2024,
|
||||
month: 3,
|
||||
start_date: '2024-03-01',
|
||||
end_date: '2024-03-31',
|
||||
status: 'active',
|
||||
is_current: true,
|
||||
is_active: true,
|
||||
evaluation_focus: JSON.stringify(['創新性', '技術性', '實用性']),
|
||||
max_team_size: 5
|
||||
},
|
||||
{
|
||||
id: uuidv4(),
|
||||
name: '2024年團隊協作競賽',
|
||||
description: '團隊協作開發的AI應用',
|
||||
type: 'team',
|
||||
year: 2024,
|
||||
month: 4,
|
||||
start_date: '2024-04-01',
|
||||
end_date: '2024-04-30',
|
||||
status: 'upcoming',
|
||||
is_current: false,
|
||||
is_active: true,
|
||||
evaluation_focus: JSON.stringify(['團隊協作', '技術實現', '創新應用']),
|
||||
max_team_size: 8
|
||||
}
|
||||
];
|
||||
|
||||
for (const competition of competitions) {
|
||||
await connection.execute(
|
||||
`INSERT INTO competitions (id, name, description, type, year, month, start_date, end_date, status, is_current, is_active, evaluation_focus, max_team_size, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())`,
|
||||
[competition.id, competition.name, competition.description, competition.type, competition.year, competition.month,
|
||||
competition.start_date, competition.end_date, competition.status, competition.is_current, competition.is_active,
|
||||
competition.evaluation_focus, competition.max_team_size]
|
||||
);
|
||||
}
|
||||
console.log(`✅ 創建了 ${competitions.length} 個競賽`);
|
||||
|
||||
// 4. 創建示例應用
|
||||
console.log('📱 創建示例應用...');
|
||||
const apps = [
|
||||
{
|
||||
id: uuidv4(),
|
||||
name: '智能對話助手',
|
||||
description: '基於大語言模型的智能對話系統',
|
||||
creator_id: users[0].id,
|
||||
team_id: null,
|
||||
category: '文字處理',
|
||||
technology_stack: JSON.stringify(['Python', 'OpenAI API', 'React']),
|
||||
github_url: 'https://github.com/example/chatbot',
|
||||
demo_url: 'https://demo.example.com/chatbot',
|
||||
status: 'published',
|
||||
is_active: true,
|
||||
total_likes: 25,
|
||||
total_views: 150
|
||||
},
|
||||
{
|
||||
id: uuidv4(),
|
||||
name: '圖像生成工具',
|
||||
description: 'AI驅動的創意圖像生成平台',
|
||||
creator_id: users[1].id,
|
||||
team_id: null,
|
||||
category: '圖像生成',
|
||||
technology_stack: JSON.stringify(['Python', 'Stable Diffusion', 'FastAPI']),
|
||||
github_url: 'https://github.com/example/image-gen',
|
||||
demo_url: 'https://demo.example.com/image-gen',
|
||||
status: 'published',
|
||||
is_active: true,
|
||||
total_likes: 18,
|
||||
total_views: 120
|
||||
},
|
||||
{
|
||||
id: uuidv4(),
|
||||
name: '語音識別系統',
|
||||
description: '高精度多語言語音識別服務',
|
||||
creator_id: users[2].id,
|
||||
team_id: null,
|
||||
category: '語音辨識',
|
||||
technology_stack: JSON.stringify(['Python', 'Whisper', 'Docker']),
|
||||
github_url: 'https://github.com/example/speech-recognition',
|
||||
demo_url: 'https://demo.example.com/speech',
|
||||
status: 'published',
|
||||
is_active: true,
|
||||
total_likes: 32,
|
||||
total_views: 200
|
||||
}
|
||||
];
|
||||
|
||||
for (const app of apps) {
|
||||
await connection.execute(
|
||||
`INSERT INTO apps (id, name, description, creator_id, team_id, category, type, app_url, likes_count, views_count, is_active, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())`,
|
||||
[app.id, app.name, app.description, app.creator_id, app.team_id, app.category, 'web', app.demo_url, app.total_likes, app.total_views, 1]
|
||||
);
|
||||
}
|
||||
console.log(`✅ 創建了 ${apps.length} 個應用`);
|
||||
|
||||
// 5. 關聯競賽和應用
|
||||
console.log('🔗 關聯競賽和應用...');
|
||||
const currentCompetition = competitions[0]; // 當前競賽
|
||||
for (const app of apps) {
|
||||
await connection.execute(
|
||||
`INSERT INTO competition_apps (id, competition_id, app_id, submitted_at) VALUES (?, ?, ?, NOW())`,
|
||||
[uuidv4(), currentCompetition.id, app.id]
|
||||
);
|
||||
}
|
||||
console.log(`✅ 關聯了 ${apps.length} 個應用到當前競賽`);
|
||||
|
||||
// 6. 關聯競賽和評審
|
||||
console.log('🔗 關聯競賽和評審...');
|
||||
for (const judge of judges) {
|
||||
await connection.execute(
|
||||
`INSERT INTO competition_judges (id, competition_id, judge_id, assigned_at) VALUES (?, ?, ?, NOW())`,
|
||||
[uuidv4(), currentCompetition.id, judge.id]
|
||||
);
|
||||
}
|
||||
console.log(`✅ 關聯了 ${judges.length} 個評審到當前競賽`);
|
||||
|
||||
// 7. 創建示例評分
|
||||
console.log('📊 創建示例評分...');
|
||||
for (const app of apps) {
|
||||
for (const judge of judges) {
|
||||
const scores = {
|
||||
innovation_score: Math.floor(Math.random() * 5) + 1,
|
||||
technical_score: Math.floor(Math.random() * 5) + 1,
|
||||
usability_score: Math.floor(Math.random() * 5) + 1,
|
||||
presentation_score: Math.floor(Math.random() * 5) + 1,
|
||||
impact_score: Math.floor(Math.random() * 5) + 1
|
||||
};
|
||||
|
||||
const totalScore = (scores.innovation_score + scores.technical_score + scores.usability_score +
|
||||
scores.presentation_score + scores.impact_score) / 5;
|
||||
|
||||
await connection.execute(
|
||||
`INSERT INTO app_judge_scores (id, judge_id, app_id, innovation_score, technical_score, usability_score, presentation_score, impact_score, total_score, comments, submitted_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())`,
|
||||
[uuidv4(), judge.id, app.id, scores.innovation_score, scores.technical_score, scores.usability_score,
|
||||
scores.presentation_score, scores.impact_score, totalScore, '示例評分']
|
||||
);
|
||||
}
|
||||
}
|
||||
console.log(`✅ 創建了 ${apps.length * judges.length} 個評分記錄`);
|
||||
|
||||
console.log('🎉 示例數據填充完成!');
|
||||
console.log('\n📊 數據摘要:');
|
||||
console.log(`- 用戶: ${users.length} 個`);
|
||||
console.log(`- 評審: ${judges.length} 個`);
|
||||
console.log(`- 競賽: ${competitions.length} 個 (其中 1 個為當前競賽)`);
|
||||
console.log(`- 應用: ${apps.length} 個`);
|
||||
console.log(`- 評分記錄: ${apps.length * judges.length} 個`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 填充失敗:', error.message);
|
||||
console.error('詳細錯誤:', error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
if (connection) {
|
||||
await connection.end();
|
||||
console.log('🔌 資料庫連接已關閉');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 執行填充
|
||||
if (require.main === module) {
|
||||
populateSampleData().catch(console.error);
|
||||
}
|
||||
|
||||
module.exports = { populateSampleData };
|
Reference in New Issue
Block a user