feat: OTA热更新 — 版本检测API + App端WGT下载安装 + app_versions表

This commit is contained in:
2026-08-07 11:49:59 +08:00
parent 0df7215134
commit bff8787e62
7 changed files with 278 additions and 1 deletions

View File

@ -0,0 +1,36 @@
"""add_app_versions
Revision ID: c9d0e1f2a3b4
Revises: b8c9d0e1f2a3
Create Date: 2026-08-07
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
revision: str = "c9d0e1f2a3b4"
down_revision: Union[str, None] = "b8c9d0e1f2a3"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.create_table(
"app_versions",
sa.Column("id", sa.UUID(), primary_key=True, server_default=sa.text("gen_random_uuid()")),
sa.Column("version", sa.String(20), nullable=False, unique=True, comment="版本号"),
sa.Column("version_code", sa.Integer(), nullable=False, server_default="100", comment="数字版本号"),
sa.Column("wgt_url", sa.String(500), nullable=False, comment="WGT下载地址"),
sa.Column("description", sa.Text(), nullable=True, comment="更新说明"),
sa.Column("is_active", sa.Boolean(), server_default=sa.text("true"), comment="是否启用"),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
# 插入初始版本记录
op.execute(
"INSERT INTO app_versions (version, version_code, wgt_url, description, is_active) "
"VALUES ('T1.0.1', 101, '', '初始版本', true)"
)
def downgrade() -> None:
op.drop_table("app_versions")

View File

@ -0,0 +1,55 @@
"""App 版本更新 API — OTA 热更新检测"""
from fastapi import APIRouter, Depends, Query
from sqlalchemy import select, desc
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
from app.models.app_version import AppVersion
from app.schemas.app_version import AppVersionResponse
router = APIRouter(prefix="/app", tags=["App版本"])
@router.get("/version", response_model=AppVersionResponse)
async def check_version(
current: str = Query(..., description="当前 App 版本号,如 T1.0.0"),
db: AsyncSession = Depends(get_db),
):
"""检测是否有新版本可用"""
# 查询最新的激活版本
result = await db.execute(
select(AppVersion)
.where(AppVersion.is_active.is_(True))
.order_by(desc(AppVersion.version_code))
.limit(1)
)
latest = result.scalar_one_or_none()
if not latest:
# 无版本记录 → 无更新
return AppVersionResponse(
version=current,
version_code=0,
has_update=False,
)
# 比较版本号
has_update = latest.version_code > _parse_version_code(current)
return AppVersionResponse(
version=latest.version,
version_code=latest.version_code,
has_update=has_update,
wgt_url=latest.wgt_url if has_update else None,
description=latest.description if has_update else None,
force_update=False,
)
def _parse_version_code(version_str: str) -> int:
"""从版本字符串提取数字版本号兜底用最后3位"""
import re
nums = re.findall(r"\d+", version_str)
if nums:
return int("".join(nums[-3:]).ljust(3, "0")[:3])
return 0

View File

@ -11,6 +11,7 @@ from app.api.v1.endpoints.users import router as users_router
from app.api.v1.endpoints.upload import router as upload_router
from app.api.v1.endpoints.records import router as records_router
from app.api.v1.endpoints.notifications import router as notifications_router
from app.api.v1.endpoints.app_version import router as app_version_router
api_router = APIRouter()
@ -25,3 +26,4 @@ api_router.include_router(users_router)
api_router.include_router(upload_router)
api_router.include_router(records_router)
api_router.include_router(notifications_router)
api_router.include_router(app_version_router)

View File

@ -5,6 +5,7 @@ from app.models.product import Product
from app.models.task import Task, TaskRecord
from app.models.task_log import TaskLog
from app.models.notification import Notification
from app.models.app_version import AppVersion
__all__ = [
"Base",
"ProductionOrder",
@ -13,4 +14,5 @@ __all__ = [
"TaskRecord",
"TaskLog",
"Notification",
"AppVersion",
]

View File

@ -0,0 +1,44 @@
"""App 版本管理模型 — 用于 OTA 热更新"""
import uuid
from datetime import datetime
from sqlalchemy import String, DateTime, Boolean, Text, Integer
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column
from app.models.base import Base
from app.core.time_utils import get_beijing_time
class AppVersion(Base):
__tablename__ = "app_versions"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4,
)
version: Mapped[str] = mapped_column(
String(20), nullable=False, unique=True, comment="版本号,如 T1.0.1",
)
version_code: Mapped[int] = mapped_column(
Integer, nullable=False, default=100, comment="数字版本号,用于比较",
)
wgt_url: Mapped[str] = mapped_column(
String(500), nullable=False, comment="WGT 升级包下载地址",
)
description: Mapped[str | None] = mapped_column(
Text, nullable=True, comment="更新说明",
)
is_active: Mapped[bool] = mapped_column(
Boolean, default=True, comment="是否启用",
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=get_beijing_time,
)
def __repr__(self) -> str:
return f"<AppVersion {self.version}>"

View File

@ -0,0 +1,14 @@
"""App 版本 Schema"""
from pydantic import BaseModel
class AppVersionResponse(BaseModel):
"""返回给 App 的版本信息"""
version: str
version_code: int
has_update: bool
wgt_url: str | None = None
description: str | None = None
force_update: bool = False
model_config = {"from_attributes": True}