""" 客户端版本发布管理接口 """ from datetime import datetime from typing import Optional from fastapi import APIRouter, Depends, Query, Request from pydantic import BaseModel, Field from sqlalchemy.orm import Session from app.api.deps import get_current_admin from app.database import get_db from app.models.client_app_version import ClientAppVersion from app.utils.timezone import beijing_now from app.utils.history import create_operation_log router = APIRouter() class ClientVersionCreate(BaseModel): platform: str = Field(..., description="平台: windows/macos/linux") channel: str = Field("stable", description="通道: stable/beta") version: str = Field(..., description="版本号") build_number: Optional[int] = Field(None, description="构建号") release_title: Optional[str] = Field(None, description="发布标题") release_notes_html: Optional[str] = Field("", description="富文本日志") download_url: Optional[str] = Field("", description="下载地址") download_hash: Optional[str] = Field("", description="下载hash") is_force_update: bool = Field(False, description="是否强更") min_supported_version: Optional[str] = Field(None, description="最低支持版本") status: str = Field("draft", description="状态: draft/published/offline") published_at: Optional[str] = Field(None, description="发布时间") class ClientVersionUpdate(BaseModel): platform: Optional[str] = None channel: Optional[str] = None version: Optional[str] = None build_number: Optional[int] = None release_title: Optional[str] = None release_notes_html: Optional[str] = None download_url: Optional[str] = None download_hash: Optional[str] = None is_force_update: Optional[bool] = None min_supported_version: Optional[str] = None status: Optional[str] = None published_at: Optional[str] = None def _format_row(row: ClientAppVersion) -> dict: return { "id": row.id, "platform": row.platform, "channel": row.channel, "version": row.version, "build_number": row.build_number, "release_title": row.release_title, "release_notes_html": row.release_notes_html or "", "download_url": row.download_url or "", "download_hash": row.download_hash or "", "is_force_update": bool(row.is_force_update), "min_supported_version": row.min_supported_version, "status": row.status, "published_at": row.published_at.isoformat() if row.published_at else None, "created_by": row.created_by, "updated_by": row.updated_by, "created_at": row.created_at.isoformat() if row.created_at else None, "updated_at": row.updated_at.isoformat() if row.updated_at else None, } @router.get("") async def get_client_versions( page: int = Query(1, ge=1), page_size: int = Query(10, ge=1, le=100), platform: Optional[str] = Query(None), channel: Optional[str] = Query(None), status: Optional[str] = Query(None), keyword: Optional[str] = Query(None), db: Session = Depends(get_db), current_admin=Depends(get_current_admin), ): query = db.query(ClientAppVersion).filter(ClientAppVersion.is_deleted == 0) if platform: query = query.filter(ClientAppVersion.platform == platform.strip().lower()) if channel: query = query.filter(ClientAppVersion.channel == channel.strip().lower()) if status: query = query.filter(ClientAppVersion.status == status.strip().lower()) if keyword: kw = f"%{keyword}%" query = query.filter( (ClientAppVersion.version.like(kw)) | (ClientAppVersion.release_title.like(kw)) ) total = query.count() rows = query.order_by(ClientAppVersion.id.desc()).offset((page - 1) * page_size).limit(page_size).all() return { "code": 200, "msg": "成功", "data": [_format_row(r) for r in rows], "total": total, "page": page, "page_size": page_size, } @router.get("/{version_id}") async def get_client_version_detail( version_id: int, db: Session = Depends(get_db), current_admin=Depends(get_current_admin), ): row = db.query(ClientAppVersion).filter( ClientAppVersion.id == version_id, ClientAppVersion.is_deleted == 0 ).first() if not row: return {"code": 404, "msg": "版本记录不存在"} return {"code": 200, "msg": "成功", "data": _format_row(row)} @router.post("") async def create_client_version( req: ClientVersionCreate, request: Request, db: Session = Depends(get_db), current_admin=Depends(get_current_admin), ): exists = db.query(ClientAppVersion).filter( ClientAppVersion.platform == req.platform.strip().lower(), ClientAppVersion.channel == req.channel.strip().lower(), ClientAppVersion.version == req.version.strip(), ClientAppVersion.is_deleted == 0 ).first() if exists: return {"code": 400, "msg": "同平台同通道版本已存在"} publish_time = None if req.published_at: try: publish_time = datetime.fromisoformat(req.published_at.replace("Z", "+00:00")) except Exception: publish_time = beijing_now() if req.status == "published" and publish_time is None: publish_time = beijing_now() row = ClientAppVersion( platform=req.platform.strip().lower(), channel=req.channel.strip().lower(), version=req.version.strip(), build_number=req.build_number, release_title=(req.release_title or "").strip() or None, release_notes_html=req.release_notes_html or "", download_url=(req.download_url or "").strip(), download_hash=(req.download_hash or "").strip(), is_force_update=1 if req.is_force_update else 0, min_supported_version=(req.min_supported_version or "").strip() or None, status=req.status.strip().lower(), published_at=publish_time, created_by=current_admin.username, updated_by=current_admin.username, created_at=beijing_now(), updated_at=beijing_now(), ) db.add(row) db.flush() client_ip = request.client.host if request.client else None create_operation_log( db=db, admin_id=current_admin.id, module="客户端版本", action="创建版本", description=f"创建客户端版本:{row.platform}/{row.channel}/{row.version}", new_data={"id": row.id, "version": row.version, "status": row.status}, ip_address=client_ip, ) db.commit() db.refresh(row) return {"code": 200, "msg": "创建成功", "data": _format_row(row)} @router.put("/{version_id}") async def update_client_version( version_id: int, req: ClientVersionUpdate, request: Request, db: Session = Depends(get_db), current_admin=Depends(get_current_admin), ): row = db.query(ClientAppVersion).filter( ClientAppVersion.id == version_id, ClientAppVersion.is_deleted == 0 ).first() if not row: return {"code": 404, "msg": "版本记录不存在"} old_data = _format_row(row) payload = req.dict(exclude_unset=True) if "platform" in payload and payload["platform"] is not None: row.platform = str(payload["platform"]).strip().lower() if "channel" in payload and payload["channel"] is not None: row.channel = str(payload["channel"]).strip().lower() if "version" in payload and payload["version"] is not None: row.version = str(payload["version"]).strip() if "build_number" in payload: row.build_number = payload["build_number"] if "release_title" in payload: row.release_title = (payload["release_title"] or "").strip() or None if "release_notes_html" in payload: row.release_notes_html = payload["release_notes_html"] or "" if "download_url" in payload: row.download_url = (payload["download_url"] or "").strip() if "download_hash" in payload: row.download_hash = (payload["download_hash"] or "").strip() if "is_force_update" in payload and payload["is_force_update"] is not None: row.is_force_update = 1 if payload["is_force_update"] else 0 if "min_supported_version" in payload: row.min_supported_version = (payload["min_supported_version"] or "").strip() or None if "status" in payload and payload["status"] is not None: row.status = str(payload["status"]).strip().lower() if row.status == "published" and not row.published_at: row.published_at = beijing_now() if "published_at" in payload: val = payload["published_at"] if val: try: row.published_at = datetime.fromisoformat(str(val).replace("Z", "+00:00")) except Exception: row.published_at = beijing_now() else: row.published_at = None row.updated_by = current_admin.username row.updated_at = beijing_now() client_ip = request.client.host if request.client else None create_operation_log( db=db, admin_id=current_admin.id, module="客户端版本", action="更新版本", description=f"更新客户端版本:{row.platform}/{row.channel}/{row.version}", old_data=old_data, new_data=_format_row(row), ip_address=client_ip, ) db.commit() return {"code": 200, "msg": "更新成功", "data": _format_row(row)} @router.delete("/{version_id}") async def delete_client_version( version_id: int, request: Request, db: Session = Depends(get_db), current_admin=Depends(get_current_admin), ): row = db.query(ClientAppVersion).filter( ClientAppVersion.id == version_id, ClientAppVersion.is_deleted == 0 ).first() if not row: return {"code": 404, "msg": "版本记录不存在"} old_data = _format_row(row) row.is_deleted = 1 row.updated_by = current_admin.username row.updated_at = beijing_now() client_ip = request.client.host if request.client else None create_operation_log( db=db, admin_id=current_admin.id, module="客户端版本", action="删除版本", description=f"删除客户端版本:{row.platform}/{row.channel}/{row.version}", old_data=old_data, ip_address=client_ip, ) db.commit() return {"code": 200, "msg": "删除成功"}