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 from app.crawler.huawei_api import HuaweiAPI from app.crawler.data_processor import DataProcessor router = APIRouter(prefix="/apps", tags=["应用"]) @router.get("/fetch/{pkg_name}") async def fetch_app_by_pkg_name( pkg_name: str, db: AsyncSession = Depends(get_db) ): """通过包名从华为API获取应用信息并保存""" api = HuaweiAPI() try: # 从华为API获取数据 print(f"正在获取应用信息: {pkg_name}") app_data = await api.get_app_info(pkg_name=pkg_name) # 获取评分数据 rating_data = await api.get_app_rating(app_data['appId']) # 保存到数据库 processor = DataProcessor(db) new_info, new_metric, new_rating = await processor.save_app_data( app_data, rating_data ) return ApiResponse( success=True, data={ "app_id": app_data['appId'], "name": app_data['name'], "pkg_name": app_data['pkgName'], "new_info": new_info, "new_metric": new_metric, "new_rating": new_rating, "message": "应用信息获取成功" } ) except Exception as e: raise HTTPException(status_code=500, detail=f"获取应用信息失败: {str(e)}") finally: await api.close() @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 )) ) # 如果是元服务分类,只显示元服务(packing_type = 1) if category == "元服务": query = query.where(AppInfo.packing_type == 1) else: # 其他分类排除元服务,并按kind_name筛选 query = query.where(and_( AppInfo.kind_name == category, or_(AppInfo.packing_type != 1, AppInfo.packing_type.is_(None)) )) query = query.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)): """获取所有分类""" # 获取元服务数量 atomic_service_result = await db.execute( select(func.count(AppInfo.app_id)) .where(AppInfo.packing_type == 1) ) atomic_service_count = atomic_service_result.scalar() # 获取其他分类(排除元服务) result = await db.execute( select(AppInfo.kind_name, func.count(AppInfo.app_id).label('count')) .where(or_(AppInfo.packing_type != 1, AppInfo.packing_type.is_(None))) .group_by(AppInfo.kind_name) .order_by(func.count(AppInfo.app_id).desc()) ) rows = result.all() data = [] # 如果有元服务,添加到列表首位 if atomic_service_count > 0: data.append({"name": "元服务", "count": atomic_service_count}) # 添加其他分类 data.extend([{"name": row[0], "count": row[1]} for row in rows]) return ApiResponse(success=True, data=data) @router.get("/by-date") async def get_apps_by_date( date: str = Query(..., description="日期格式: YYYY-MM-DD"), page_size: int = Query(100, le=100), db: AsyncSession = Depends(get_db) ): """获取指定日期上架的应用""" try: from datetime import datetime, time # 解析日期字符串 target_date = datetime.strptime(date, '%Y-%m-%d') date_start = datetime.combine(target_date, time.min) date_end = datetime.combine(target_date, time.max) # 获取最新的指标记录 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(and_( AppInfo.listed_at >= date_start, AppInfo.listed_at <= date_end )) .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 and row[1] else 0, "version": row[1].version if len(row) > 1 and row[1] else "", "average_rating": float(row[2].average_rating) if len(row) > 2 and row[2] else 0.0, "total_rating_count": row[2].total_rating_count if len(row) > 2 and row[2] else 0, "listed_at": row[0].listed_at.isoformat() if row[0].listed_at else "" } for row in rows] return ApiResponse(success=True, data=data, total=len(data)) except ValueError as e: raise HTTPException(status_code=400, detail=f"日期格式错误: {str(e)}") except Exception as e: print(f"Error in get_apps_by_date: {e}") import traceback traceback.print_exc() return ApiResponse(success=True, data=[], total=0) @router.get("/today") async def get_today_apps( page_size: int = Query(100, le=100), db: AsyncSession = Depends(get_db) ): """获取今日上架应用(根据 listed_at 字段判断是否为今天上架)""" try: # 获取今天的日期范围(00:00:00 到 23:59:59) from datetime import datetime, time today_start = datetime.combine(datetime.today(), time.min) today_end = datetime.combine(datetime.today(), time.max) # 获取最新的指标记录 subquery = ( select(AppMetrics.app_id, func.max(AppMetrics.created_at).label('max_created_at')) .group_by(AppMetrics.app_id) .subquery() ) # 查询今天上架的应用(根据 listed_at 字段) 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(and_( AppInfo.listed_at >= today_start, AppInfo.listed_at <= today_end )) .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 and row[1] else 0, "version": row[1].version if len(row) > 1 and row[1] else "", "average_rating": float(row[2].average_rating) if len(row) > 2 and row[2] else 0.0, "total_rating_count": row[2].total_rating_count if len(row) > 2 and row[2] else 0, "listed_at": row[0].listed_at.isoformat() if row[0].listed_at else "" } for row in rows] return ApiResponse(success=True, data=data, total=len(data)) except Exception as e: print(f"Error in get_today_apps: {e}") import traceback traceback.print_exc() # 返回空列表而不是抛出错误 return ApiResponse(success=True, data=[], total=0) @router.get("/top-downloads") async def get_top_downloads( 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_rating_created_at')) .group_by(AppRating.app_id) .subquery() ) query = ( select(AppInfo, AppMetrics, AppRating) .join(AppMetrics, AppInfo.app_id == AppMetrics.app_id) .join(subquery_metric, and_( AppMetrics.app_id == subquery_metric.c.app_id, AppMetrics.created_at == subquery_metric.c.max_created_at )) .outerjoin(subquery_rating, AppInfo.app_id == subquery_rating.c.app_id) .outerjoin(AppRating, and_( AppInfo.app_id == AppRating.app_id, AppRating.created_at == subquery_rating.c.max_rating_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, "dev_id": row[0].dev_id, "supplier": row[0].supplier, # 分类信息 "kind_name": row[0].kind_name, "kind_id": row[0].kind_id, "tag_name": row[0].tag_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, "price": row[0].price, # 时间信息 "listed_at": row[0].listed_at.isoformat(), # 设备支持 "main_device_codes": row[0].main_device_codes or [], # SDK信息 "target_sdk": row[0].target_sdk, "min_sdk": row[0].min_sdk, "compile_sdk_version": row[0].compile_sdk_version, "min_hmos_api_level": row[0].min_hmos_api_level, "api_release_type": row[0].api_release_type, # 其他信息 "ctype": row[0].ctype, "app_level": row[0].app_level, "packing_type": row[0].packing_type, # 版本和指标信息 "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)