應用 APP 功能實作

This commit is contained in:
2025-09-09 18:18:02 +08:00
parent 22bbe64349
commit 900e33aefa
22 changed files with 2745 additions and 242 deletions

View File

@@ -917,61 +917,552 @@ export class CompetitionService {
// =====================================================
export class AppService {
// 創建應用
static async createApp(appData: Omit<App, 'id' | 'created_at' | 'updated_at'>): Promise<App> {
async createApp(appData: {
name: string;
description: string;
creator_id: string;
category: string;
type: string;
app_url?: string;
icon?: string;
icon_color?: string;
}): Promise<{ success: boolean; app?: any; error?: string }> {
try {
const appId = crypto.randomUUID();
const sql = `
INSERT INTO apps (id, name, description, creator_id, category, type, app_url, icon, icon_color, likes_count, views_count, rating, is_active, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0, 0.00, TRUE, NOW(), NOW())
`;
await this.query(sql, [
appId,
appData.name,
appData.description,
appData.creator_id,
appData.category,
appData.type,
appData.app_url || null,
appData.icon || 'Bot',
appData.icon_color || 'from-blue-500 to-purple-500'
]);
// 獲取創建的應用
const createdApp = await this.getAppById(appId);
return { success: true, app: createdApp };
} catch (error) {
console.error('創建應用錯誤:', error);
return { success: false, error: '創建應用時發生錯誤' };
}
}
// 根據 ID 獲取應用(僅已發布)
async getAppById(appId: string): Promise<any | null> {
const sql = `
INSERT INTO apps (id, name, description, creator_id, team_id, category, type, likes_count, views_count, rating, is_active)
VALUES (UUID(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
SELECT
a.*,
u.name as creator_name,
u.department as creator_department
FROM apps a
LEFT JOIN users u ON a.creator_id = u.id
WHERE a.id = ? AND a.is_active = TRUE
`;
const params = [
appData.name,
appData.description || null,
appData.creator_id,
appData.team_id || null,
appData.category,
appData.type,
appData.likes_count,
appData.views_count,
appData.rating,
appData.is_active
];
await db.insert(sql, params);
return await this.getAppByName(appData.name) as App;
return await this.queryOne(sql, [appId]);
}
// 根據 ID 獲取應用(任何狀態)
async getAppByIdAnyStatus(appId: string): Promise<any | null> {
const sql = `
SELECT
a.*,
u.name as creator_name,
u.department as creator_department
FROM apps a
LEFT JOIN users u ON a.creator_id = u.id
WHERE a.id = ?
`;
return await this.queryOne(sql, [appId]);
}
// 根據名稱獲取應用
static async getAppByName(name: string): Promise<App | null> {
const sql = 'SELECT * FROM apps WHERE name = ? AND is_active = TRUE';
return await db.queryOne<App>(sql, [name]);
}
// 根據ID獲取應用
static async getAppById(id: string): Promise<App | null> {
const sql = 'SELECT * FROM apps WHERE id = ? AND is_active = TRUE';
return await db.queryOne<App>(sql, [id]);
async getAppByName(name: string): Promise<any | null> {
const sql = `
SELECT
a.*,
u.name as creator_name,
u.department as creator_department
FROM apps a
LEFT JOIN users u ON a.creator_id = u.id
WHERE a.name = ? AND a.is_active = TRUE
`;
return await this.queryOne(sql, [name]);
}
// 獲取所有應用
static async getAllApps(limit = 50, offset = 0): Promise<App[]> {
const sql = 'SELECT * FROM apps WHERE is_active = TRUE ORDER BY created_at DESC LIMIT ? OFFSET ?';
return await db.query<App>(sql, [limit, offset]);
}
async getAllApps(filters: {
search?: string;
category?: string;
type?: string;
status?: string;
page?: number;
limit?: number;
} = {}): Promise<{ apps: any[]; total: number }> {
try {
const { search = '', category = 'all', type = 'all', status = 'all', page = 1, limit = 10 } = filters;
// 構建查詢條件
let whereConditions: string[] = [];
let params: any[] = [];
// 獲取應用統計
static async getAppStatistics(id: string): Promise<AppStatistics | null> {
const sql = 'SELECT * FROM app_statistics WHERE id = ?';
return await db.queryOne<AppStatistics>(sql, [id]);
// 根據狀態篩選
if (status && status !== 'all') {
if (status === 'active') {
whereConditions.push('a.is_active = TRUE');
} else if (status === 'inactive') {
whereConditions.push('a.is_active = FALSE');
}
// 如果 status 是 'all' 或其他值,則不添加狀態篩選
}
if (search) {
whereConditions.push('(a.name LIKE ? OR a.description LIKE ? OR u.name LIKE ?)');
params.push(`%${search}%`, `%${search}%`, `%${search}%`);
}
if (category && category !== 'all') {
whereConditions.push('a.category = ?');
params.push(category);
}
if (type && type !== 'all') {
whereConditions.push('a.type = ?');
params.push(type);
}
const whereClause = whereConditions.length > 0 ? `WHERE ${whereConditions.join(' AND ')}` : '';
// 獲取總數
const countSql = `
SELECT COUNT(DISTINCT a.id) as total
FROM apps a
LEFT JOIN users u ON a.creator_id = u.id
LEFT JOIN user_ratings ur ON a.id = ur.app_id
${whereClause}
`;
const countResult = await this.queryOne(countSql, params);
const total = countResult?.total || 0;
// 獲取應用列表
const offset = (page - 1) * limit;
const sql = `
SELECT
a.*,
u.name as creator_name,
u.department as creator_department,
u.email as creator_email,
COALESCE(AVG(ur.rating), 0) as rating,
COUNT(ur.id) as reviewCount
FROM apps a
LEFT JOIN users u ON a.creator_id = u.id
LEFT JOIN user_ratings ur ON a.id = ur.app_id
${whereClause}
GROUP BY a.id, u.name, u.department, u.email
ORDER BY a.created_at DESC
LIMIT ${limit} OFFSET ${offset}
`;
const apps = await this.query(sql, params);
return { apps, total };
} catch (error) {
console.error('獲取應用列表錯誤:', error);
return { apps: [], total: 0 };
}
}
// 更新應用
static async updateApp(id: string, updates: Partial<App>): Promise<boolean> {
const fields = Object.keys(updates).filter(key => key !== 'id' && key !== 'created_at');
const setClause = fields.map(field => `${field} = ?`).join(', ');
const values = fields.map(field => (updates as any)[field]);
const sql = `UPDATE apps SET ${setClause}, updated_at = CURRENT_TIMESTAMP WHERE id = ?`;
const result = await db.update(sql, [...values, id]);
return result.affectedRows > 0;
async updateApp(appId: string, updates: {
name?: string;
description?: string;
category?: string;
type?: string;
app_url?: string;
icon?: string;
icon_color?: string;
}): Promise<{ success: boolean; app?: any; error?: string }> {
try {
const updateFields = [];
const params = [];
if (updates.name !== undefined) {
updateFields.push('name = ?');
params.push(updates.name);
}
if (updates.description !== undefined) {
updateFields.push('description = ?');
params.push(updates.description);
}
if (updates.category !== undefined) {
updateFields.push('category = ?');
params.push(updates.category);
}
if (updates.type !== undefined) {
updateFields.push('type = ?');
params.push(updates.type);
}
if (updates.app_url !== undefined) {
updateFields.push('app_url = ?');
params.push(updates.app_url);
}
if (updates.icon !== undefined) {
updateFields.push('icon = ?');
params.push(updates.icon);
}
if (updates.icon_color !== undefined) {
updateFields.push('icon_color = ?');
params.push(updates.icon_color);
}
if (updateFields.length === 0) {
return { success: false, error: '沒有要更新的欄位' };
}
updateFields.push('updated_at = NOW()');
params.push(appId);
const sql = `
UPDATE apps
SET ${updateFields.join(', ')}
WHERE id = ? AND is_active = TRUE
`;
await this.query(sql, params);
// 獲取更新後的應用
const updatedApp = await this.getAppById(appId);
return { success: true, app: updatedApp };
} catch (error) {
console.error('更新應用錯誤:', error);
return { success: false, error: '更新應用時發生錯誤' };
}
}
// 刪除應用(硬刪除)
async deleteApp(appId: string): Promise<{ success: boolean; error?: string }> {
try {
// 先檢查應用是否存在(任何狀態)
const app = await this.getAppByIdAnyStatus(appId);
if (!app) {
return { success: false, error: '應用不存在' };
}
// 硬刪除應用(從資料庫中完全移除)
const sql = 'DELETE FROM apps WHERE id = ?';
await this.query(sql, [appId]);
return { success: true };
} catch (error) {
console.error('刪除應用錯誤:', error);
return { success: false, error: '刪除應用時發生錯誤' };
}
}
// 切換應用狀態
async toggleAppStatus(appId: string): Promise<{ success: boolean; app?: any; error?: string }> {
try {
// 先獲取當前狀態(任何狀態)
const currentApp = await this.getAppByIdAnyStatus(appId);
if (!currentApp) {
return { success: false, error: '應用不存在' };
}
const newStatus = currentApp.is_active ? false : true;
const sql = 'UPDATE apps SET is_active = ?, updated_at = NOW() WHERE id = ?';
await this.query(sql, [newStatus, appId]);
// 獲取更新後的應用(任何狀態)
const updatedApp = await this.getAppByIdAnyStatus(appId);
return { success: true, app: updatedApp };
} catch (error) {
console.error('切換應用狀態錯誤:', error);
return { success: false, error: '切換應用狀態時發生錯誤' };
}
}
// 獲取應用統計
async getAppStats(): Promise<{
totalApps: number;
activeApps: number;
inactiveApps: number;
pendingApps: number;
totalViews: number;
totalLikes: number;
newThisMonth: number;
}> {
try {
const sql = `
SELECT
COUNT(*) as total_apps,
COUNT(CASE WHEN is_active = TRUE THEN 1 END) as active_apps,
COUNT(CASE WHEN is_active = FALSE THEN 1 END) as inactive_apps,
COALESCE(SUM(views_count), 0) as total_views,
COALESCE(SUM(likes_count), 0) as total_likes,
COUNT(CASE WHEN created_at >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) THEN 1 END) as new_this_month
FROM apps
`;
const result = await this.queryOne(sql);
return {
totalApps: result?.total_apps || 0,
activeApps: result?.active_apps || 0,
inactiveApps: result?.inactive_apps || 0,
pendingApps: 0, // 目前沒有 pending 狀態
totalViews: result?.total_views || 0,
totalLikes: result?.total_likes || 0,
newThisMonth: result?.new_this_month || 0
};
} catch (error) {
console.error('獲取應用統計錯誤:', error);
return {
totalApps: 0,
activeApps: 0,
inactiveApps: 0,
pendingApps: 0,
totalViews: 0,
totalLikes: 0,
newThisMonth: 0
};
}
}
// 通用查詢方法
async query<T = any>(sql: string, params: any[] = []): Promise<T[]> {
return await db.query<T>(sql, params);
}
// 通用單一查詢方法
async queryOne<T = any>(sql: string, params: any[] = []): Promise<T | null> {
return await db.queryOne<T>(sql, params);
}
// 獲取應用評分統計
async getAppRatingStats(appId: string): Promise<{
averageRating: number;
totalRatings: number;
ratingDistribution: { rating: number; count: number }[];
}> {
try {
const sql = `
SELECT
AVG(rating) as average_rating,
COUNT(*) as total_ratings
FROM user_ratings
WHERE app_id = ?
`;
const result = await this.queryOne(sql, [appId]);
const distributionSql = `
SELECT
rating,
COUNT(*) as count
FROM user_ratings
WHERE app_id = ?
GROUP BY rating
ORDER BY rating DESC
`;
const distribution = await this.query(distributionSql, [appId]);
return {
averageRating: result.average_rating ? parseFloat(result.average_rating) : 0,
totalRatings: result.total_ratings || 0,
ratingDistribution: distribution.map((row: any) => ({
rating: row.rating,
count: row.count
}))
};
} catch (error) {
console.error('獲取應用評分統計錯誤:', error);
return {
averageRating: 0,
totalRatings: 0,
ratingDistribution: []
};
}
}
// 獲取應用使用統計
async getAppUsageStats(appId: string): Promise<{
dailyUsers: number;
weeklyUsers: number;
monthlyUsers: number;
totalSessions: number;
topDepartments: { department: string; count: number }[];
trendData: { date: string; users: number }[];
}> {
try {
// 今日使用者
const dailySql = `
SELECT COUNT(DISTINCT user_id) as daily_users
FROM user_views
WHERE app_id = ? AND DATE(viewed_at) = CURDATE()
`;
const dailyResult = await this.queryOne(dailySql, [appId]);
// 本週使用者
const weeklySql = `
SELECT COUNT(DISTINCT user_id) as weekly_users
FROM user_views
WHERE app_id = ? AND viewed_at >= DATE_SUB(CURDATE(), INTERVAL 1 WEEK)
`;
const weeklyResult = await this.queryOne(weeklySql, [appId]);
// 本月使用者
const monthlySql = `
SELECT COUNT(DISTINCT user_id) as monthly_users
FROM user_views
WHERE app_id = ? AND viewed_at >= DATE_SUB(CURDATE(), INTERVAL 1 MONTH)
`;
const monthlyResult = await this.queryOne(monthlySql, [appId]);
// 總使用次數
const totalSql = `
SELECT COUNT(*) as total_sessions
FROM user_views
WHERE app_id = ?
`;
const totalResult = await this.queryOne(totalSql, [appId]);
// 部門使用統計
const deptSql = `
SELECT
u.department,
COUNT(*) as count
FROM user_views uv
JOIN users u ON uv.user_id = u.id
WHERE uv.app_id = ?
GROUP BY u.department
ORDER BY count DESC
LIMIT 5
`;
const deptResult = await this.query(deptSql, [appId]);
// 使用趨勢過去7天
const trendSql = `
SELECT
DATE(viewed_at) as date,
COUNT(DISTINCT user_id) as users
FROM user_views
WHERE app_id = ? AND viewed_at >= DATE_SUB(CURDATE(), INTERVAL 7 DAY)
GROUP BY DATE(viewed_at)
ORDER BY date ASC
`;
const trendResult = await this.query(trendSql, [appId]);
return {
dailyUsers: dailyResult.daily_users || 0,
weeklyUsers: weeklyResult.weekly_users || 0,
monthlyUsers: monthlyResult.monthly_users || 0,
totalSessions: totalResult.total_sessions || 0,
topDepartments: deptResult.map((row: any) => ({
department: row.department,
count: row.count
})),
trendData: trendResult.map((row: any) => ({
date: row.date,
users: row.users
}))
};
} catch (error) {
console.error('獲取應用使用統計錯誤:', error);
return {
dailyUsers: 0,
weeklyUsers: 0,
monthlyUsers: 0,
totalSessions: 0,
topDepartments: [],
trendData: []
};
}
}
// 獲取應用評價列表
async getAppReviews(appId: string, limit: number = 10, offset: number = 0): Promise<any[]> {
try {
const sql = `
SELECT
ur.id,
ur.rating,
ur.rated_at,
u.name as user_name,
u.department as user_department,
u.avatar as user_avatar
FROM user_ratings ur
JOIN users u ON ur.user_id = u.id
WHERE ur.app_id = ?
ORDER BY ur.rated_at DESC
LIMIT ${limit} OFFSET ${offset}
`;
const reviews = await this.query(sql, [appId]);
return reviews.map((review: any) => ({
id: review.id,
rating: review.rating,
review: '用戶評價', // 暫時使用固定文字,因為資料庫中沒有 review 欄位
ratedAt: review.rated_at,
userName: review.user_name,
userDepartment: review.user_department,
userAvatar: review.user_avatar
}));
} catch (error) {
console.error('獲取應用評價列表錯誤:', error);
return [];
}
}
// 獲取應用評價總數
async getAppReviewCount(appId: string): Promise<number> {
try {
const sql = `
SELECT COUNT(*) as count
FROM user_ratings
WHERE app_id = ?
`;
const result = await this.queryOne(sql, [appId]);
return result.count || 0;
} catch (error) {
console.error('獲取應用評價總數錯誤:', error);
return 0;
}
}
// 刪除評價
async deleteReview(reviewId: string, appId: string): Promise<{ success: boolean; error?: string }> {
try {
// 檢查評價是否存在且屬於該應用
const checkSql = `
SELECT id FROM user_ratings
WHERE id = ? AND app_id = ?
`;
const review = await this.queryOne(checkSql, [reviewId, appId]);
if (!review) {
return { success: false, error: '評價不存在或無權限刪除' };
}
// 刪除評價
const deleteSql = 'DELETE FROM user_ratings WHERE id = ?';
await this.query(deleteSql, [reviewId]);
return { success: true };
} catch (error) {
console.error('刪除評價錯誤:', error);
return { success: false, error: '刪除評價時發生錯誤' };
}
}
}