340 lines
9.2 KiB
TypeScript
340 lines
9.2 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server';
|
|
import { db } from '@/lib/database';
|
|
import { authenticateUser, requireDeveloperOrAdmin } from '@/lib/auth';
|
|
import { logger } from '@/lib/logger';
|
|
import { AppSearchParams, AppCreateRequest } from '@/types/app';
|
|
|
|
// GET /api/apps - 獲取應用程式列表
|
|
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 { searchParams } = new URL(request.url);
|
|
const search = searchParams.get('search') || '';
|
|
const type = searchParams.get('type') || '';
|
|
const status = searchParams.get('status') || '';
|
|
const creatorId = searchParams.get('creatorId') || '';
|
|
const teamId = searchParams.get('teamId') || '';
|
|
const page = parseInt(searchParams.get('page') || '1');
|
|
const limit = parseInt(searchParams.get('limit') || '10');
|
|
|
|
// 確保參數是數字類型
|
|
const limitNum = Number(limit);
|
|
const offsetNum = Number((page - 1) * limit);
|
|
const sortBy = searchParams.get('sortBy') || 'created_at';
|
|
const sortOrder = searchParams.get('sortOrder') || 'desc';
|
|
|
|
// 構建查詢條件
|
|
const conditions: string[] = [];
|
|
const params: any[] = [];
|
|
|
|
if (search) {
|
|
conditions.push('(a.name LIKE ? OR a.description LIKE ? OR u.name LIKE ?)');
|
|
const searchTerm = `%${search}%`;
|
|
params.push(searchTerm, searchTerm, searchTerm);
|
|
}
|
|
|
|
if (type) {
|
|
conditions.push('a.type = ?');
|
|
params.push(type);
|
|
}
|
|
|
|
if (status) {
|
|
conditions.push('a.status = ?');
|
|
params.push(status);
|
|
}
|
|
|
|
if (creatorId) {
|
|
conditions.push('a.creator_id = ?');
|
|
params.push(creatorId);
|
|
}
|
|
|
|
if (teamId) {
|
|
conditions.push('a.team_id = ?');
|
|
params.push(teamId);
|
|
}
|
|
|
|
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
|
|
// 計算總數
|
|
const countSql = `
|
|
SELECT COUNT(*) as total
|
|
FROM apps a
|
|
LEFT JOIN users u ON a.creator_id = u.id
|
|
${whereClause}
|
|
`;
|
|
|
|
const totalResults = await db.query<{ total: number }>(countSql, params);
|
|
const total = totalResults.length > 0 ? totalResults[0].total : 0;
|
|
|
|
// 計算分頁
|
|
const totalPages = Math.ceil(total / limit);
|
|
|
|
// 計算各狀態的統計
|
|
const statsSql = `
|
|
SELECT a.status, COUNT(*) as count
|
|
FROM apps a
|
|
LEFT JOIN users u ON a.creator_id = u.id
|
|
${whereClause}
|
|
GROUP BY a.status
|
|
`;
|
|
const statsResults = await db.query(statsSql, params);
|
|
const stats = {
|
|
published: 0,
|
|
pending: 0,
|
|
draft: 0,
|
|
rejected: 0
|
|
};
|
|
statsResults.forEach((row: any) => {
|
|
if (stats.hasOwnProperty(row.status)) {
|
|
stats[row.status] = row.count;
|
|
}
|
|
});
|
|
|
|
|
|
|
|
// 構建排序
|
|
const validSortFields = ['name', 'created_at', 'rating', 'likes_count', 'views_count'];
|
|
const validSortOrders = ['asc', 'desc'];
|
|
const finalSortBy = validSortFields.includes(sortBy) ? sortBy : 'created_at';
|
|
const finalSortOrder = validSortOrders.includes(sortOrder) ? sortOrder : 'desc';
|
|
|
|
// 查詢應用程式列表
|
|
const sql = `
|
|
SELECT
|
|
a.*,
|
|
u.name as creator_name,
|
|
u.email as creator_email,
|
|
u.department as creator_department,
|
|
u.role as creator_role
|
|
FROM apps a
|
|
LEFT JOIN users u ON a.creator_id = u.id
|
|
${whereClause}
|
|
ORDER BY a.created_at DESC
|
|
LIMIT ${limitNum} OFFSET ${offsetNum}
|
|
`;
|
|
|
|
const apps = await db.query(sql, params);
|
|
|
|
// 格式化回應資料
|
|
const formattedApps = apps.map((app: any) => ({
|
|
id: app.id,
|
|
name: app.name,
|
|
description: app.description,
|
|
creatorId: app.creator_id,
|
|
teamId: app.team_id,
|
|
status: app.status,
|
|
type: app.type,
|
|
filePath: app.file_path,
|
|
techStack: app.tech_stack ? JSON.parse(app.tech_stack) : [],
|
|
tags: app.tags ? JSON.parse(app.tags) : [],
|
|
screenshots: app.screenshots ? JSON.parse(app.screenshots) : [],
|
|
demoUrl: app.demo_url,
|
|
githubUrl: app.github_url,
|
|
docsUrl: app.docs_url,
|
|
version: app.version,
|
|
likesCount: app.likes_count,
|
|
viewsCount: app.views_count,
|
|
rating: app.rating,
|
|
createdAt: app.created_at,
|
|
updatedAt: app.updated_at,
|
|
lastUpdated: app.last_updated,
|
|
creator: {
|
|
id: app.creator_id,
|
|
name: app.creator_name,
|
|
email: app.creator_email,
|
|
department: app.creator_department,
|
|
role: app.creator_role
|
|
},
|
|
team: app.team_id ? {
|
|
id: app.team_id,
|
|
name: app.team_name,
|
|
department: app.team_department,
|
|
contactEmail: app.team_contact_email
|
|
} : undefined
|
|
}));
|
|
|
|
const duration = Date.now() - startTime;
|
|
logger.logRequest('GET', '/api/apps', 200, duration, user.id);
|
|
|
|
return NextResponse.json({
|
|
apps: formattedApps,
|
|
pagination: {
|
|
page,
|
|
limit,
|
|
total,
|
|
totalPages,
|
|
hasNext: page < totalPages,
|
|
hasPrev: page > 1
|
|
},
|
|
stats
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.logError(error as Error, 'Apps API - GET');
|
|
|
|
const duration = Date.now() - startTime;
|
|
logger.logRequest('GET', '/api/apps', 500, duration);
|
|
|
|
console.error('詳細錯誤信息:', error);
|
|
|
|
return NextResponse.json(
|
|
{
|
|
error: '獲取應用程式列表失敗',
|
|
details: error instanceof Error ? error.message : '未知錯誤'
|
|
},
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|
|
|
|
// POST /api/apps - 創建新應用程式
|
|
export async function POST(request: NextRequest) {
|
|
const startTime = Date.now();
|
|
|
|
try {
|
|
// 驗證用戶權限
|
|
const user = await requireDeveloperOrAdmin(request);
|
|
|
|
const body = await request.json();
|
|
const {
|
|
name,
|
|
description,
|
|
type,
|
|
teamId,
|
|
techStack,
|
|
tags,
|
|
demoUrl,
|
|
githubUrl,
|
|
docsUrl,
|
|
version = '1.0.0'
|
|
}: AppCreateRequest = body;
|
|
|
|
// 驗證必填欄位
|
|
if (!name || !description || !type) {
|
|
return NextResponse.json(
|
|
{ error: '請提供應用程式名稱、描述和類型' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// 驗證應用程式名稱長度
|
|
if (name.length < 2 || name.length > 200) {
|
|
return NextResponse.json(
|
|
{ error: '應用程式名稱長度必須在 2-200 個字符之間' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// 驗證描述長度
|
|
if (description.length < 10) {
|
|
return NextResponse.json(
|
|
{ error: '應用程式描述至少需要 10 個字符' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// 驗證類型
|
|
const validTypes = [
|
|
'web_app', 'mobile_app', 'desktop_app', 'api_service', 'ai_model',
|
|
'data_analysis', 'automation', 'productivity', 'educational', 'healthcare',
|
|
'finance', 'iot_device', 'blockchain', 'ar_vr', 'machine_learning',
|
|
'computer_vision', 'nlp', 'robotics', 'cybersecurity', 'cloud_service', 'other'
|
|
];
|
|
if (!validTypes.includes(type)) {
|
|
return NextResponse.json(
|
|
{ error: '無效的應用程式類型' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// 如果指定了團隊,驗證團隊存在且用戶是團隊成員
|
|
if (teamId) {
|
|
const teamMember = await db.queryOne(
|
|
'SELECT * FROM team_members WHERE team_id = ? AND user_id = ?',
|
|
[teamId, user.id]
|
|
);
|
|
|
|
if (!teamMember) {
|
|
return NextResponse.json(
|
|
{ error: '您不是該團隊的成員,無法為該團隊創建應用程式' },
|
|
{ status: 403 }
|
|
);
|
|
}
|
|
}
|
|
|
|
// 生成應用程式 ID
|
|
const appId = Date.now().toString(36) + Math.random().toString(36).substr(2);
|
|
|
|
// 準備插入資料
|
|
const appData = {
|
|
id: appId,
|
|
name,
|
|
description,
|
|
creator_id: user.id,
|
|
team_id: teamId || null,
|
|
type,
|
|
tech_stack: techStack ? JSON.stringify(techStack) : null,
|
|
tags: tags ? JSON.stringify(tags) : null,
|
|
demo_url: demoUrl || null,
|
|
github_url: githubUrl || null,
|
|
docs_url: docsUrl || null,
|
|
version,
|
|
status: 'draft'
|
|
};
|
|
|
|
// 插入應用程式
|
|
await db.insert('apps', appData);
|
|
|
|
// 記錄活動
|
|
logger.logActivity(user.id, 'app', appId, 'create', {
|
|
name,
|
|
type,
|
|
teamId
|
|
});
|
|
|
|
const duration = Date.now() - startTime;
|
|
logger.logRequest('POST', '/api/apps', 201, duration, user.id);
|
|
|
|
return NextResponse.json({
|
|
message: '應用程式創建成功',
|
|
appId,
|
|
app: {
|
|
id: appId,
|
|
name,
|
|
description,
|
|
type,
|
|
status: 'draft',
|
|
creatorId: user.id,
|
|
teamId,
|
|
version
|
|
}
|
|
}, { status: 201 });
|
|
|
|
} catch (error) {
|
|
logger.logError(error as Error, 'Apps API - POST');
|
|
|
|
const duration = Date.now() - startTime;
|
|
logger.logRequest('POST', '/api/apps', 500, duration);
|
|
|
|
console.error('詳細錯誤信息:', error);
|
|
|
|
return NextResponse.json(
|
|
{
|
|
error: '創建應用程式失敗',
|
|
details: error instanceof Error ? error.message : '未知錯誤'
|
|
},
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|