feat: 添加 App 检查更新功能

- 前端:设置页面添加检查更新菜单项,从 pubspec.yaml 动态获取版本号
- 后端:新增 /api/v1/app/check-updates 接口,自动扫描 releases 目录对比版本
- 配置:新增 AppVersionSettings,支持通过环境变量配置版本和下载链接
- Docker:添加 releases 目录挂载
This commit is contained in:
qzl
2026-03-16 16:09:07 +08:00
parent dcceb48d84
commit ab073c88ed
11 changed files with 485 additions and 138 deletions
+20
View File
@@ -198,6 +198,25 @@ class DatabaseSettings(BaseModel):
)
class AppVersionSettings(BaseModel):
releases_dir: str = Field(
default="releases",
description="安装包目录,相对于项目根目录",
)
current_version: str = Field(
default="0.1.0",
description="当前版本号",
)
current_build: int = Field(
default=1,
description="当前构建号",
)
download_base_url: str = Field(
default="",
description="下载链接基础域名,如 https://your-domain.com",
)
def _resolve_env_file() -> str:
current = Path(__file__).resolve()
for parent in [current, *current.parents]:
@@ -221,6 +240,7 @@ class Settings(BaseSettings):
agent_runtime: AgentRuntimeSettings = AgentRuntimeSettings()
taskiq: TaskiqSettings = TaskiqSettings()
database: DatabaseSettings = DatabaseSettings()
app_version: AppVersionSettings = AppVersionSettings()
@computed_field
@property
+116
View File
@@ -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
View File
@@ -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)