| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285 |
- """
- 客户端版本发布管理接口
- """
- 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": "删除成功"}
|