from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, func, and_, or_ from datetime import datetime, timedelta from typing import Optional from app.database import get_db from app.models import AppInfo, AppMetrics, AppRating from app.schemas import ApiResponse router = APIRouter(prefix="/apps", tags=["应用"]) @router.get("/search") async def search_apps( q: str = Query(..., min_length=1), page: int = Query(1, ge=1), page_size: int = Query(20, le=100), db: AsyncSession = Depends(get_db) ): """搜索应用""" subquery = ( select(AppMetrics.app_id, func.max(AppMetrics.created_at).label('max_created_at')) .group_by(AppMetrics.app_id) .subquery() ) query = ( select(AppInfo, AppMetrics, AppRating) .join(AppMetrics, AppInfo.app_id == AppMetrics.app_id) .outerjoin(AppRating, AppInfo.app_id == AppRating.app_id) .join(subquery, and_( AppMetrics.app_id == subquery.c.app_id, AppMetrics.created_at == subquery.c.max_created_at )) .where(or_( AppInfo.name.like(f"%{q}%"), AppInfo.pkg_name.like(f"%{q}%"), AppInfo.developer_name.like(f"%{q}%") )) .order_by(AppMetrics.download_count.desc()) ) count_query = select(func.count(AppInfo.app_id)).where(or_( AppInfo.name.like(f"%{q}%"), AppInfo.pkg_name.like(f"%{q}%"), AppInfo.developer_name.like(f"%{q}%") )) total_result = await db.execute(count_query) total = total_result.scalar() offset = (page - 1) * page_size query = query.offset(offset).limit(page_size) result = await db.execute(query) rows = result.all() data = [{ "app_id": row[0].app_id, "name": row[0].name, "pkg_name": row[0].pkg_name, "developer_name": row[0].developer_name, "kind_name": row[0].kind_name, "icon_url": row[0].icon_url, "brief_desc": row[0].brief_desc, "download_count": row[1].download_count if len(row) > 1 else 0, "version": row[1].version if len(row) > 1 else "", "average_rating": float(row[2].average_rating) if len(row) > 2 and row[2] else 0, "listed_at": row[0].listed_at.isoformat() } for row in rows] return ApiResponse(success=True, data=data, total=total, limit=page_size) @router.get("/category/{category}") async def get_apps_by_category( category: str, page: int = Query(1, ge=1), page_size: int = Query(20, le=100), db: AsyncSession = Depends(get_db) ): """按分类获取应用""" subquery = ( select(AppMetrics.app_id, func.max(AppMetrics.created_at).label('max_created_at')) .group_by(AppMetrics.app_id) .subquery() ) query = ( select(AppInfo, AppMetrics, AppRating) .join(AppMetrics, AppInfo.app_id == AppMetrics.app_id) .outerjoin(AppRating, AppInfo.app_id == AppRating.app_id) .join(subquery, and_( AppMetrics.app_id == subquery.c.app_id, AppMetrics.created_at == subquery.c.max_created_at )) .where(AppInfo.kind_name == category) .order_by(AppMetrics.download_count.desc()) ) count_query = select(func.count(AppInfo.app_id)).where(AppInfo.kind_name == category) total_result = await db.execute(count_query) total = total_result.scalar() offset = (page - 1) * page_size query = query.offset(offset).limit(page_size) result = await db.execute(query) rows = result.all() data = [{ "app_id": row[0].app_id, "name": row[0].name, "pkg_name": row[0].pkg_name, "developer_name": row[0].developer_name, "kind_name": row[0].kind_name, "icon_url": row[0].icon_url, "brief_desc": row[0].brief_desc, "download_count": row[1].download_count if len(row) > 1 else 0, "version": row[1].version if len(row) > 1 else "", "average_rating": float(row[2].average_rating) if len(row) > 2 and row[2] else 0, "listed_at": row[0].listed_at.isoformat() } for row in rows] return ApiResponse(success=True, data=data, total=total, limit=page_size) @router.get("/categories") async def get_categories(db: AsyncSession = Depends(get_db)): """获取所有分类""" result = await db.execute( select(AppInfo.kind_name, func.count(AppInfo.app_id).label('count')) .group_by(AppInfo.kind_name) .order_by(func.count(AppInfo.app_id).desc()) ) rows = result.all() data = [{"name": row[0], "count": row[1]} for row in rows] return ApiResponse(success=True, data=data) @router.get("/today") async def get_today_apps( page_size: int = Query(20, le=100), db: AsyncSession = Depends(get_db) ): """获取今日上架应用""" today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) subquery = ( select(AppMetrics.app_id, func.max(AppMetrics.created_at).label('max_created_at')) .group_by(AppMetrics.app_id) .subquery() ) query = ( select(AppInfo, AppMetrics, AppRating) .join(AppMetrics, AppInfo.app_id == AppMetrics.app_id) .outerjoin(AppRating, AppInfo.app_id == AppRating.app_id) .join(subquery, and_( AppMetrics.app_id == subquery.c.app_id, AppMetrics.created_at == subquery.c.max_created_at )) .where(AppInfo.listed_at >= today) .order_by(AppInfo.listed_at.desc()) .limit(page_size) ) result = await db.execute(query) rows = result.all() data = [{ "app_id": row[0].app_id, "name": row[0].name, "pkg_name": row[0].pkg_name, "developer_name": row[0].developer_name, "kind_name": row[0].kind_name, "icon_url": row[0].icon_url, "brief_desc": row[0].brief_desc, "download_count": row[1].download_count if len(row) > 1 else 0, "version": row[1].version if len(row) > 1 else "", "average_rating": float(row[2].average_rating) if len(row) > 2 and row[2] else 0, "listed_at": row[0].listed_at.isoformat() } for row in rows] return ApiResponse(success=True, data=data, total=len(data)) @router.get("/top-downloads") async def get_top_downloads( limit: int = Query(100, le=100), db: AsyncSession = Depends(get_db) ): """热门应用Top100""" subquery = ( select(AppMetrics.app_id, func.max(AppMetrics.created_at).label('max_created_at')) .group_by(AppMetrics.app_id) .subquery() ) query = ( select(AppInfo, AppMetrics, AppRating) .join(AppMetrics, AppInfo.app_id == AppMetrics.app_id) .outerjoin(AppRating, AppInfo.app_id == AppRating.app_id) .join(subquery, and_( AppMetrics.app_id == subquery.c.app_id, AppMetrics.created_at == subquery.c.max_created_at )) .order_by(AppMetrics.download_count.desc()) .limit(limit) ) result = await db.execute(query) rows = result.all() data = [{ "app_id": row[0].app_id, "name": row[0].name, "pkg_name": row[0].pkg_name, "developer_name": row[0].developer_name, "kind_name": row[0].kind_name, "icon_url": row[0].icon_url, "brief_desc": row[0].brief_desc, "download_count": row[1].download_count if len(row) > 1 else 0, "version": row[1].version if len(row) > 1 else "", "average_rating": float(row[2].average_rating) if len(row) > 2 and row[2] else 0 } for row in rows] return ApiResponse(success=True, data=data, total=len(data)) @router.get("/top-ratings") async def get_top_ratings( limit: int = Query(100, le=100), db: AsyncSession = Depends(get_db) ): """评分Top100""" subquery_metric = ( select(AppMetrics.app_id, func.max(AppMetrics.created_at).label('max_created_at')) .group_by(AppMetrics.app_id) .subquery() ) subquery_rating = ( select(AppRating.app_id, func.max(AppRating.created_at).label('max_created_at')) .group_by(AppRating.app_id) .subquery() ) query = ( select(AppInfo, AppMetrics, AppRating) .join(AppMetrics, AppInfo.app_id == AppMetrics.app_id) .join(AppRating, AppInfo.app_id == AppRating.app_id) .join(subquery_metric, and_( AppMetrics.app_id == subquery_metric.c.app_id, AppMetrics.created_at == subquery_metric.c.max_created_at )) .join(subquery_rating, and_( AppRating.app_id == subquery_rating.c.app_id, AppRating.created_at == subquery_rating.c.max_created_at )) .where(AppRating.total_rating_count >= 100) .order_by(AppRating.average_rating.desc()) .limit(limit) ) result = await db.execute(query) rows = result.all() data = [{ "app_id": row[0].app_id, "name": row[0].name, "pkg_name": row[0].pkg_name, "developer_name": row[0].developer_name, "kind_name": row[0].kind_name, "icon_url": row[0].icon_url, "brief_desc": row[0].brief_desc, "download_count": row[1].download_count if len(row) > 1 else 0, "version": row[1].version if len(row) > 1 else "", "average_rating": float(row[2].average_rating) if len(row) > 2 and row[2] else 0, "total_rating_count": row[2].total_rating_count if len(row) > 2 and row[2] else 0 } for row in rows] return ApiResponse(success=True, data=data, total=len(data)) @router.get("/{app_id}") async def get_app_detail(app_id: str, db: AsyncSession = Depends(get_db)): """获取应用详情""" subquery = ( select(AppMetrics.app_id, func.max(AppMetrics.created_at).label('max_created_at')) .where(AppMetrics.app_id == app_id) .group_by(AppMetrics.app_id) .subquery() ) query = ( select(AppInfo, AppMetrics, AppRating) .join(AppMetrics, AppInfo.app_id == AppMetrics.app_id) .outerjoin(AppRating, AppInfo.app_id == AppRating.app_id) .join(subquery, and_( AppMetrics.app_id == subquery.c.app_id, AppMetrics.created_at == subquery.c.max_created_at )) .where(AppInfo.app_id == app_id) ) result = await db.execute(query) row = result.first() if not row: raise HTTPException(status_code=404, detail="应用不存在") data = { "app_id": row[0].app_id, "name": row[0].name, "pkg_name": row[0].pkg_name, "developer_name": row[0].developer_name, "kind_name": row[0].kind_name, "icon_url": row[0].icon_url, "brief_desc": row[0].brief_desc, "description": row[0].description, "privacy_url": row[0].privacy_url, "is_pay": row[0].is_pay, "listed_at": row[0].listed_at.isoformat(), "download_count": row[1].download_count if len(row) > 1 else 0, "version": row[1].version if len(row) > 1 else "", "size_bytes": row[1].size_bytes if len(row) > 1 else 0, "average_rating": float(row[2].average_rating) if len(row) > 2 and row[2] else 0, "total_rating_count": row[2].total_rating_count if len(row) > 2 and row[2] else 0, "star_1_count": row[2].star_1_count if len(row) > 2 and row[2] else 0, "star_2_count": row[2].star_2_count if len(row) > 2 and row[2] else 0, "star_3_count": row[2].star_3_count if len(row) > 2 and row[2] else 0, "star_4_count": row[2].star_4_count if len(row) > 2 and row[2] else 0, "star_5_count": row[2].star_5_count if len(row) > 2 and row[2] else 0 } return ApiResponse(success=True, data=data)