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:
Nvex
2025-10-25 21:20:32 +08:00
parent c0f81dbbe2
commit 720402ffe7
38 changed files with 5682 additions and 407 deletions

View File

@@ -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,