應用 APP 功能實作
This commit is contained in:
@@ -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: '刪除評價時發生錯誤' };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user