Files
meijiaka-zy/python-api/app/api/v1/update.py
T
小鱼开发 6c64189c70 fix(update): check_update 兼容同平台多包,优先返回 updater 包
- scalar_one_or_none() 在 release_id+platform+architecture 多行时抛异常
- 改为查询全部后取第一个,按 signature desc 排序优先 updater 包
2026-05-26 23:28:09 +08:00

195 lines
5.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
应用更新 API
============
为 Tauri updater 插件提供更新检查接口。
"""
from datetime import UTC, datetime
from fastapi import APIRouter, Depends, HTTPException, Query, Response, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.session import get_db
from app.models.update import AppRelease, ReleasePackage
from app.schemas.update import (
ReleaseCreate,
ReleaseListItem,
ReleaseResponse,
TauriPlatformInfo,
TauriUpdateResponse,
)
router = APIRouter()
@router.get("/check", response_model=TauriUpdateResponse)
async def check_update(
version: str = Query(..., description="当前应用版本"),
target: str = Query(..., description="平台:darwin / windows / linux"),
arch: str = Query(..., description="架构:x86_64 / aarch64"),
db: AsyncSession = Depends(get_db),
):
"""
检查应用更新 — Tauri updater 插件入口
Tauri 启动时会向此接口发送请求,携带当前版本、平台、架构信息。
如果无需更新,返回 204;如果有更新,返回 Tauri 标准格式的 JSON。
"""
# 查询最新版本
result = await db.execute(
select(AppRelease).order_by(AppRelease.release_date.desc()).limit(1)
)
latest: AppRelease | None = result.scalar_one_or_none()
if not latest:
return Response(status_code=status.HTTP_204_NO_CONTENT)
# 已是最新版本(或更高)
if latest.version == version:
return Response(status_code=status.HTTP_204_NO_CONTENT)
# 查询对应平台的包(优先返回 updater 用的包:有 signature 的 .app.tar.gz / .exe
result = await db.execute(
select(ReleasePackage).where(
ReleasePackage.release_id == latest.id,
ReleasePackage.platform == target,
ReleasePackage.architecture == arch,
).order_by(
# 有 signature 的排前面(updater 包),空 signature 的排后面(dmg 安装包)
ReleasePackage.signature.desc()
)
)
pkgs = result.scalars().all()
# 取第一个:优先有 signature 的 updater 包;如果没有则取任意一个
pkg: ReleasePackage | None = pkgs[0] if pkgs else None
if not pkg:
# 该平台无包,返回 204(避免报错阻断用户)
return Response(status_code=status.HTTP_204_NO_CONTENT)
# 构建 Tauri 格式的响应
platform_key = f"{target}-{arch}"
return TauriUpdateResponse(
version=latest.version,
notes=latest.notes,
pub_date=latest.release_date.isoformat() if latest.release_date else None,
mandatory=latest.mandatory,
platforms={
platform_key: TauriPlatformInfo(
url=pkg.file_url,
signature=pkg.signature,
)
},
)
@router.post("/releases", response_model=ReleaseResponse, status_code=status.HTTP_201_CREATED)
async def create_release(
release: ReleaseCreate,
db: AsyncSession = Depends(get_db),
):
"""
创建新版本发布(管理员接口)
用于手动发版时,将构建产物信息写入数据库。
"""
# 检查版本是否已存在
result = await db.execute(select(AppRelease).where(AppRelease.version == release.version))
if result.scalar_one_or_none():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"版本 {release.version} 已存在",
)
# 创建发布记录
new_release = AppRelease(
version=release.version,
release_date=datetime.now(UTC),
notes=release.notes,
mandatory=release.mandatory,
)
db.add(new_release)
await db.flush()
# 创建包记录
for pkg in release.packages:
db.add(
ReleasePackage(
release_id=new_release.id,
platform=pkg.platform,
architecture=pkg.architecture,
filename=pkg.filename,
file_url=pkg.file_url,
file_size=pkg.file_size,
signature=pkg.signature,
)
)
await db.commit()
await db.refresh(new_release)
return ReleaseResponse(
id=new_release.id,
version=new_release.version,
release_date=new_release.release_date,
notes=new_release.notes,
mandatory=new_release.mandatory,
created_at=new_release.created_at,
packages=[
{
"platform": p.platform,
"architecture": p.architecture,
"filename": p.filename,
"file_url": p.file_url,
"file_size": p.file_size,
"signature": p.signature,
}
for p in new_release.packages
],
)
@router.get("/releases", response_model=list[ReleaseListItem])
async def list_releases(
db: AsyncSession = Depends(get_db),
):
"""获取所有版本发布列表(管理员接口)"""
result = await db.execute(select(AppRelease).order_by(AppRelease.release_date.desc()))
releases = result.scalars().all()
return [
ReleaseListItem(
id=r.id,
version=r.version,
release_date=r.release_date,
notes=r.notes,
mandatory=r.mandatory,
package_count=len(r.packages),
)
for r in releases
]
@router.delete("/releases/{version}")
async def delete_release(
version: str,
db: AsyncSession = Depends(get_db),
):
"""删除版本发布(管理员接口)"""
result = await db.execute(select(AppRelease).where(AppRelease.version == version))
release = result.scalar_one_or_none()
if not release:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"版本 {version} 不存在",
)
await db.delete(release)
await db.commit()
return {"status": "success", "message": f"版本 {version} 已删除"}