feat: NEXT Store 2.0 重大更新 - 完整重构前后端
🎉 主要更新:
后端:
- 全新华为应用市场爬虫系统
- 三表分离数据库设计 (app_info, app_metrics, app_rating)
- 完整的API接口 (搜索、分类、热门、上新等)
- 元服务自动识别和分类
- 智能Token管理和数据处理
- 修复热门应用重复显示问题
前端:
- 全新首页设计 (今日上架、热门应用)
- 应用页面 (彩色分类磁贴、智能图标匹配)
- 今日上新页面 (日期切换)
- 热门应用页面 (卡片布局)
- 应用详情页面 (完整信息展示)
- Apple风格搜索栏
- Footer组件
- 底部导航栏优化 (4个导航项)
- 骨架屏加载效果
- FontAwesome图标集成
UI/UX:
- 统一浅色背景 (#F5F5F7)
- 流畅的过渡动画
- 响应式设计
- 毛玻璃效果
文档:
- CHANGELOG.md - 完整更新日志
- QUICKSTART.md - 快速开始
- 多个技术文档和使用指南
版本: v2.0.0
This commit is contained in:
@@ -6,9 +6,50 @@ 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),
|
||||
@@ -84,6 +125,7 @@ async def get_apps_by_category(
|
||||
.subquery()
|
||||
)
|
||||
|
||||
# 构建基础查询
|
||||
query = (
|
||||
select(AppInfo, AppMetrics, AppRating)
|
||||
.join(AppMetrics, AppInfo.app_id == AppMetrics.app_id)
|
||||
@@ -92,10 +134,21 @@ async def get_apps_by_category(
|
||||
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())
|
||||
)
|
||||
|
||||
# 如果是元服务分类,只显示元服务(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()
|
||||
@@ -125,61 +178,160 @@ async def get_apps_by_category(
|
||||
@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 = [{"name": row[0], "count": row[1]} for row in rows]
|
||||
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(20, le=100),
|
||||
page_size: int = Query(100, 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))
|
||||
"""获取今日上架应用(根据 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(
|
||||
@@ -187,19 +339,31 @@ async def get_top_downloads(
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""热门应用Top100"""
|
||||
subquery = (
|
||||
# 最新的指标记录
|
||||
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)
|
||||
.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
|
||||
.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)
|
||||
@@ -305,20 +469,57 @@ async def get_app_detail(app_id: str, db: AsyncSession = Depends(get_db)):
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user