feat: 添加 App 检查更新功能
- 前端:设置页面添加检查更新菜单项,从 pubspec.yaml 动态获取版本号 - 后端:新增 /api/v1/app/check-updates 接口,自动扫描 releases 目录对比版本 - 配置:新增 AppVersionSettings,支持通过环境变量配置版本和下载链接 - Docker:添加 releases 目录挂载
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
|
||||
from fastapi import APIRouter, Query
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from core.config.settings import config
|
||||
|
||||
|
||||
class AppVersionInfo(BaseModel):
|
||||
has_update: bool = Field(description="是否有新版本可用")
|
||||
latest_version: str = Field(description="最新版本号,如 0.1.0")
|
||||
latest_build: int = Field(description="最新构建号")
|
||||
min_required_version: str = Field(description="强制更新版本号")
|
||||
update_type: Literal["none", "optional", "required"] = Field(
|
||||
description="更新类型: none=无更新, optional=可选更新, required=必须更新"
|
||||
)
|
||||
download_url: str | None = Field(default=None, description="安装包下载链接")
|
||||
release_notes: str | None = Field(default=None, description="版本更新说明")
|
||||
|
||||
|
||||
router = APIRouter(prefix="/app", tags=["app"])
|
||||
|
||||
|
||||
def _parse_version(filename: str) -> tuple[str, int] | None:
|
||||
pattern = r"app[-_]v?(\d+\.\d+\.\d+)\+(\d+)\.(?:apk|ipa)"
|
||||
match = re.search(pattern, filename, re.IGNORECASE)
|
||||
if match:
|
||||
version = match.group(1)
|
||||
build = int(match.group(2))
|
||||
return (version, build)
|
||||
return None
|
||||
|
||||
|
||||
def _get_latest_release(
|
||||
platform: Literal["ios", "android"],
|
||||
) -> tuple[str, int, str] | None:
|
||||
releases_dir = config.app_version.releases_dir
|
||||
base_path = Path.cwd().parent / "deploy" / "static" / releases_dir
|
||||
|
||||
if not base_path.exists():
|
||||
return None
|
||||
|
||||
ext = "ipa" if platform == "ios" else "apk"
|
||||
candidates = []
|
||||
|
||||
for f in base_path.iterdir():
|
||||
if f.is_file() and f.suffix.lstrip(".").lower() == ext.lower():
|
||||
parsed = _parse_version(f.name)
|
||||
if parsed:
|
||||
version, build = parsed
|
||||
candidates.append((version, build, f.name))
|
||||
|
||||
if not candidates:
|
||||
return None
|
||||
|
||||
candidates.sort(key=lambda x: (x[0], x[1]), reverse=True)
|
||||
return candidates[0][0], candidates[0][1], candidates[0][2]
|
||||
|
||||
|
||||
def _compare_versions(
|
||||
current_version: str, current_build: int, latest_version: str, latest_build: int
|
||||
) -> tuple[bool, Literal["none", "optional", "required"]]:
|
||||
if current_build >= latest_build:
|
||||
return False, "none"
|
||||
|
||||
if current_build < latest_build - 2:
|
||||
return True, "required"
|
||||
return True, "optional"
|
||||
|
||||
|
||||
@router.get("/check-updates", response_model=AppVersionInfo)
|
||||
async def check_updates(
|
||||
current_version: str | None = Query(None, description="前端当前版本,如 0.1.0"),
|
||||
current_build: int | None = Query(None, description="前端当前构建号,如 1"),
|
||||
platform: Literal["ios", "android"] = Query("ios", description="平台类型"),
|
||||
) -> AppVersionInfo:
|
||||
current_build = current_build or 0
|
||||
|
||||
latest = _get_latest_release(platform)
|
||||
if not latest:
|
||||
return AppVersionInfo(
|
||||
has_update=False,
|
||||
latest_version=config.app_version.current_version,
|
||||
latest_build=config.app_version.current_build,
|
||||
min_required_version=config.app_version.current_version,
|
||||
update_type="none",
|
||||
download_url=None,
|
||||
release_notes=None,
|
||||
)
|
||||
|
||||
latest_version, latest_build, filename = latest
|
||||
|
||||
has_update, update_type = _compare_versions(
|
||||
current_version or "0.0.0",
|
||||
current_build,
|
||||
latest_version,
|
||||
latest_build,
|
||||
)
|
||||
|
||||
download_url: str | None = None
|
||||
if has_update and config.app_version.download_base_url:
|
||||
download_url = f"{config.app_version.download_base_url.rstrip('/')}/{config.app_version.releases_dir}/{filename}"
|
||||
|
||||
return AppVersionInfo(
|
||||
has_update=has_update,
|
||||
latest_version=latest_version,
|
||||
latest_build=latest_build,
|
||||
min_required_version=latest_version,
|
||||
update_type=update_type,
|
||||
download_url=download_url,
|
||||
release_notes="问题修复和体验优化",
|
||||
)
|
||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
from fastapi import APIRouter
|
||||
|
||||
from v1.agent.router import router as agent_router
|
||||
from v1.app.router import router as app_router
|
||||
from v1.auth.router import router as auth_router
|
||||
from v1.friendships.router import router as friendships_router
|
||||
from v1.inbox_messages.router import router as inbox_messages_router
|
||||
@@ -12,8 +13,10 @@ from v1.users.router import router as users_router
|
||||
|
||||
|
||||
router = APIRouter(prefix="/api/v1")
|
||||
router.include_router(app_router)
|
||||
router.include_router(auth_router)
|
||||
router.include_router(agent_router)
|
||||
router.include_router(agent_router)
|
||||
router.include_router(friendships_router)
|
||||
router.include_router(users_router)
|
||||
router.include_router(schedule_items_router)
|
||||
|
||||
Reference in New Issue
Block a user