初始化鸿蒙应用展示平台项目 - 前后端分离架构
This commit is contained in:
9
backend/.env.example
Normal file
9
backend/.env.example
Normal file
@@ -0,0 +1,9 @@
|
||||
MYSQL_HOST=localhost
|
||||
MYSQL_PORT=3306
|
||||
MYSQL_USER=root
|
||||
MYSQL_PASSWORD=your_password
|
||||
MYSQL_DATABASE=huawei_market
|
||||
|
||||
API_PREFIX=/api
|
||||
DEBUG=False
|
||||
CORS_ORIGINS=["http://localhost:5173","http://localhost:3000"]
|
||||
40
backend/README.md
Normal file
40
backend/README.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# 后端 API 服务
|
||||
|
||||
基于 FastAPI 的鸿蒙应用展示平台后端服务。
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
# 创建虚拟环境
|
||||
python -m venv venv
|
||||
source venv/bin/activate
|
||||
|
||||
# 安装依赖
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## 配置
|
||||
|
||||
复制 `.env.example` 为 `.env` 并配置数据库连接:
|
||||
|
||||
```env
|
||||
MYSQL_HOST=localhost
|
||||
MYSQL_PORT=3306
|
||||
MYSQL_USER=root
|
||||
MYSQL_PASSWORD=your_password
|
||||
MYSQL_DATABASE=huawei_market
|
||||
```
|
||||
|
||||
## 运行
|
||||
|
||||
```bash
|
||||
python -m app.main
|
||||
```
|
||||
|
||||
服务将在 http://localhost:8000 启动
|
||||
|
||||
## API 文档
|
||||
|
||||
启动服务后访问:
|
||||
- Swagger UI: http://localhost:8000/docs
|
||||
- ReDoc: http://localhost:8000/redoc
|
||||
1
backend/app/__init__.py
Normal file
1
backend/app/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Backend Application
|
||||
1
backend/app/api/__init__.py
Normal file
1
backend/app/api/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# API Routes
|
||||
331
backend/app/api/apps.py
Normal file
331
backend/app/api/apps.py
Normal file
@@ -0,0 +1,331 @@
|
||||
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)
|
||||
25
backend/app/config.py
Normal file
25
backend/app/config.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from pydantic_settings import BaseSettings
|
||||
from typing import List
|
||||
|
||||
class Settings(BaseSettings):
|
||||
MYSQL_HOST: str = "localhost"
|
||||
MYSQL_PORT: int = 3306
|
||||
MYSQL_USER: str = "root"
|
||||
MYSQL_PASSWORD: str = "password"
|
||||
MYSQL_DATABASE: str = "huawei_market"
|
||||
|
||||
API_PREFIX: str = "/api"
|
||||
API_TITLE: str = "鸿蒙应用展示平台API"
|
||||
API_VERSION: str = "1.0.0"
|
||||
|
||||
DEBUG: bool = False
|
||||
CORS_ORIGINS: List[str] = ["http://localhost:5173", "http://localhost:3000"]
|
||||
|
||||
@property
|
||||
def database_url(self) -> str:
|
||||
return f"mysql+aiomysql://{self.MYSQL_USER}:{self.MYSQL_PASSWORD}@{self.MYSQL_HOST}:{self.MYSQL_PORT}/{self.MYSQL_DATABASE}"
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
|
||||
settings = Settings()
|
||||
27
backend/app/database.py
Normal file
27
backend/app/database.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from app.config import settings
|
||||
|
||||
engine = create_async_engine(
|
||||
settings.database_url,
|
||||
echo=settings.DEBUG,
|
||||
pool_size=10,
|
||||
max_overflow=20,
|
||||
pool_pre_ping=True
|
||||
)
|
||||
|
||||
AsyncSessionLocal = sessionmaker(
|
||||
engine,
|
||||
class_=AsyncSession,
|
||||
expire_on_commit=False
|
||||
)
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
async def get_db():
|
||||
async with AsyncSessionLocal() as session:
|
||||
try:
|
||||
yield session
|
||||
finally:
|
||||
await session.close()
|
||||
31
backend/app/main.py
Normal file
31
backend/app/main.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from app.config import settings
|
||||
from app.api import apps
|
||||
|
||||
app = FastAPI(
|
||||
title=settings.API_TITLE,
|
||||
version=settings.API_VERSION
|
||||
)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=settings.CORS_ORIGINS,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
app.include_router(apps.router, prefix=settings.API_PREFIX)
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {"message": "鸿蒙应用展示平台API", "version": settings.API_VERSION}
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
return {"status": "healthy"}
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run("app.main:app", host="0.0.0.0", port=8000, reload=settings.DEBUG)
|
||||
5
backend/app/models/__init__.py
Normal file
5
backend/app/models/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from app.models.app_info import AppInfo
|
||||
from app.models.app_metrics import AppMetrics
|
||||
from app.models.app_rating import AppRating
|
||||
|
||||
__all__ = ["AppInfo", "AppMetrics", "AppRating"]
|
||||
20
backend/app/models/app_info.py
Normal file
20
backend/app/models/app_info.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from sqlalchemy import Column, String, Integer, Text, DateTime, Boolean, JSON
|
||||
from sqlalchemy.sql import func
|
||||
from app.database import Base
|
||||
|
||||
class AppInfo(Base):
|
||||
__tablename__ = "app_info"
|
||||
|
||||
app_id = Column(String(50), primary_key=True)
|
||||
name = Column(String(255), nullable=False, index=True)
|
||||
pkg_name = Column(String(255), nullable=False, unique=True, index=True)
|
||||
developer_name = Column(String(255), nullable=False, index=True)
|
||||
kind_name = Column(String(100), nullable=False, index=True)
|
||||
icon_url = Column(Text, nullable=False)
|
||||
brief_desc = Column(Text, nullable=False)
|
||||
description = Column(Text, nullable=False)
|
||||
privacy_url = Column(Text, nullable=False)
|
||||
is_pay = Column(Boolean, default=False)
|
||||
listed_at = Column(DateTime, nullable=False)
|
||||
created_at = Column(DateTime, nullable=False, server_default=func.now())
|
||||
updated_at = Column(DateTime, nullable=False, server_default=func.now(), onupdate=func.now())
|
||||
15
backend/app/models/app_metrics.py
Normal file
15
backend/app/models/app_metrics.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from sqlalchemy import Column, String, Integer, BigInteger, DateTime, DECIMAL, ForeignKey
|
||||
from sqlalchemy.sql import func
|
||||
from app.database import Base
|
||||
|
||||
class AppMetrics(Base):
|
||||
__tablename__ = "app_metrics"
|
||||
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
app_id = Column(String(50), ForeignKey("app_info.app_id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
pkg_name = Column(String(255), ForeignKey("app_info.pkg_name", ondelete="CASCADE"), nullable=False, index=True)
|
||||
version = Column(String(50), nullable=False)
|
||||
size_bytes = Column(BigInteger, nullable=False)
|
||||
download_count = Column(BigInteger, nullable=False, index=True)
|
||||
release_date = Column(BigInteger, nullable=False)
|
||||
created_at = Column(DateTime, nullable=False, server_default=func.now(), index=True)
|
||||
18
backend/app/models/app_rating.py
Normal file
18
backend/app/models/app_rating.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from sqlalchemy import Column, String, Integer, BigInteger, DateTime, DECIMAL, ForeignKey
|
||||
from sqlalchemy.sql import func
|
||||
from app.database import Base
|
||||
|
||||
class AppRating(Base):
|
||||
__tablename__ = "app_rating"
|
||||
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
app_id = Column(String(50), ForeignKey("app_info.app_id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
pkg_name = Column(String(255), ForeignKey("app_info.pkg_name", ondelete="CASCADE"), nullable=False, index=True)
|
||||
average_rating = Column(DECIMAL(3, 2), nullable=False, index=True)
|
||||
star_1_count = Column(Integer, nullable=False, default=0)
|
||||
star_2_count = Column(Integer, nullable=False, default=0)
|
||||
star_3_count = Column(Integer, nullable=False, default=0)
|
||||
star_4_count = Column(Integer, nullable=False, default=0)
|
||||
star_5_count = Column(Integer, nullable=False, default=0)
|
||||
total_rating_count = Column(Integer, nullable=False, default=0)
|
||||
created_at = Column(DateTime, nullable=False, server_default=func.now(), index=True)
|
||||
3
backend/app/schemas/__init__.py
Normal file
3
backend/app/schemas/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from app.schemas.response import ApiResponse
|
||||
|
||||
__all__ = ["ApiResponse"]
|
||||
10
backend/app/schemas/response.py
Normal file
10
backend/app/schemas/response.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import Any, Optional
|
||||
from datetime import datetime
|
||||
|
||||
class ApiResponse(BaseModel):
|
||||
success: bool
|
||||
data: Any
|
||||
total: Optional[int] = None
|
||||
limit: Optional[int] = None
|
||||
timestamp: str = datetime.now().isoformat()
|
||||
7
backend/requirements.txt
Normal file
7
backend/requirements.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
fastapi==0.109.0
|
||||
uvicorn[standard]==0.27.0
|
||||
sqlalchemy==2.0.25
|
||||
aiomysql==0.2.0
|
||||
pydantic==2.5.3
|
||||
pydantic-settings==2.1.0
|
||||
python-dotenv==1.0.0
|
||||
Reference in New Issue
Block a user