Files
meijiaka-zy/python-api/app/api/v1/update.py
T
小鱼开发 cb56698836 feat: 应用自动更新系统 + 草稿箱删除 + 分类缓存优化
- 新增 Tauri 自动更新(updater 插件)
  - Rust: 集成 tauri-plugin-updater + tauri-plugin-process
  - 后端: app_releases / release_packages 表 + /update/check API
  - 前端: UpdateDialog 组件 + useUpdater hook + SystemUpdate 手动检查
  - 发版脚本: scripts/publish_release.py(扫描 .sig → 上传七牛云 → 写入数据库)
  - 配置 test 环境域名 dev.tapi.meijiaka.cn

- 草稿箱删除功能
  - DraftListItem 添加删除按钮
  - MyWorks 添加删除确认弹窗 + localProjectApi.deleteProject 调用

- 创作主题分类本地缓存
  - scriptApi.getCategoriesCached() 先读 localStorage 再静默刷新

- TermsModal tab 居中

- 更新应用图标(Big Sur 风格圆角矩形)

- 清理: 删除未使用文件 create_user.py / video-replace-mvp.py / DEPS_*.md
2026-05-15 16:41:57 +08:00

190 lines
5.5 KiB
Python

"""
应用更新 API
============
为 Tauri updater 插件提供更新检查接口。
"""
from datetime import UTC, datetime
from fastapi import APIRouter, Depends, HTTPException, Query, 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:
raise HTTPException(status_code=status.HTTP_204_NO_CONTENT)
# 已是最新版本(或更高)
if latest.version == version:
raise HTTPException(status_code=status.HTTP_204_NO_CONTENT)
# 查询对应平台的包
result = await db.execute(
select(ReleasePackage).where(
ReleasePackage.release_id == latest.id,
ReleasePackage.platform == target,
ReleasePackage.architecture == arch,
)
)
pkg: ReleasePackage | None = result.scalar_one_or_none()
if not pkg:
# 该平台无包,返回 204(避免报错阻断用户)
raise HTTPException(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} 已删除"}