client_versions.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. """
  2. 客户端版本发布管理接口
  3. """
  4. from datetime import datetime
  5. from typing import Optional
  6. from fastapi import APIRouter, Depends, Query, Request
  7. from pydantic import BaseModel, Field
  8. from sqlalchemy.orm import Session
  9. from app.api.deps import get_current_admin
  10. from app.database import get_db
  11. from app.models.client_app_version import ClientAppVersion
  12. from app.utils.timezone import beijing_now
  13. from app.utils.history import create_operation_log
  14. router = APIRouter()
  15. class ClientVersionCreate(BaseModel):
  16. platform: str = Field(..., description="平台: windows/macos/linux")
  17. channel: str = Field("stable", description="通道: stable/beta")
  18. version: str = Field(..., description="版本号")
  19. build_number: Optional[int] = Field(None, description="构建号")
  20. release_title: Optional[str] = Field(None, description="发布标题")
  21. release_notes_html: Optional[str] = Field("", description="富文本日志")
  22. download_url: Optional[str] = Field("", description="下载地址")
  23. download_hash: Optional[str] = Field("", description="下载hash")
  24. is_force_update: bool = Field(False, description="是否强更")
  25. min_supported_version: Optional[str] = Field(None, description="最低支持版本")
  26. status: str = Field("draft", description="状态: draft/published/offline")
  27. published_at: Optional[str] = Field(None, description="发布时间")
  28. class ClientVersionUpdate(BaseModel):
  29. platform: Optional[str] = None
  30. channel: Optional[str] = None
  31. version: Optional[str] = None
  32. build_number: Optional[int] = None
  33. release_title: Optional[str] = None
  34. release_notes_html: Optional[str] = None
  35. download_url: Optional[str] = None
  36. download_hash: Optional[str] = None
  37. is_force_update: Optional[bool] = None
  38. min_supported_version: Optional[str] = None
  39. status: Optional[str] = None
  40. published_at: Optional[str] = None
  41. def _format_row(row: ClientAppVersion) -> dict:
  42. return {
  43. "id": row.id,
  44. "platform": row.platform,
  45. "channel": row.channel,
  46. "version": row.version,
  47. "build_number": row.build_number,
  48. "release_title": row.release_title,
  49. "release_notes_html": row.release_notes_html or "",
  50. "download_url": row.download_url or "",
  51. "download_hash": row.download_hash or "",
  52. "is_force_update": bool(row.is_force_update),
  53. "min_supported_version": row.min_supported_version,
  54. "status": row.status,
  55. "published_at": row.published_at.isoformat() if row.published_at else None,
  56. "created_by": row.created_by,
  57. "updated_by": row.updated_by,
  58. "created_at": row.created_at.isoformat() if row.created_at else None,
  59. "updated_at": row.updated_at.isoformat() if row.updated_at else None,
  60. }
  61. @router.get("")
  62. async def get_client_versions(
  63. page: int = Query(1, ge=1),
  64. page_size: int = Query(10, ge=1, le=100),
  65. platform: Optional[str] = Query(None),
  66. channel: Optional[str] = Query(None),
  67. status: Optional[str] = Query(None),
  68. keyword: Optional[str] = Query(None),
  69. db: Session = Depends(get_db),
  70. current_admin=Depends(get_current_admin),
  71. ):
  72. query = db.query(ClientAppVersion).filter(ClientAppVersion.is_deleted == 0)
  73. if platform:
  74. query = query.filter(ClientAppVersion.platform == platform.strip().lower())
  75. if channel:
  76. query = query.filter(ClientAppVersion.channel == channel.strip().lower())
  77. if status:
  78. query = query.filter(ClientAppVersion.status == status.strip().lower())
  79. if keyword:
  80. kw = f"%{keyword}%"
  81. query = query.filter(
  82. (ClientAppVersion.version.like(kw)) |
  83. (ClientAppVersion.release_title.like(kw))
  84. )
  85. total = query.count()
  86. rows = query.order_by(ClientAppVersion.id.desc()).offset((page - 1) * page_size).limit(page_size).all()
  87. return {
  88. "code": 200,
  89. "msg": "成功",
  90. "data": [_format_row(r) for r in rows],
  91. "total": total,
  92. "page": page,
  93. "page_size": page_size,
  94. }
  95. @router.get("/{version_id}")
  96. async def get_client_version_detail(
  97. version_id: int,
  98. db: Session = Depends(get_db),
  99. current_admin=Depends(get_current_admin),
  100. ):
  101. row = db.query(ClientAppVersion).filter(
  102. ClientAppVersion.id == version_id,
  103. ClientAppVersion.is_deleted == 0
  104. ).first()
  105. if not row:
  106. return {"code": 404, "msg": "版本记录不存在"}
  107. return {"code": 200, "msg": "成功", "data": _format_row(row)}
  108. @router.post("")
  109. async def create_client_version(
  110. req: ClientVersionCreate,
  111. request: Request,
  112. db: Session = Depends(get_db),
  113. current_admin=Depends(get_current_admin),
  114. ):
  115. exists = db.query(ClientAppVersion).filter(
  116. ClientAppVersion.platform == req.platform.strip().lower(),
  117. ClientAppVersion.channel == req.channel.strip().lower(),
  118. ClientAppVersion.version == req.version.strip(),
  119. ClientAppVersion.is_deleted == 0
  120. ).first()
  121. if exists:
  122. return {"code": 400, "msg": "同平台同通道版本已存在"}
  123. publish_time = None
  124. if req.published_at:
  125. try:
  126. publish_time = datetime.fromisoformat(req.published_at.replace("Z", "+00:00"))
  127. except Exception:
  128. publish_time = beijing_now()
  129. if req.status == "published" and publish_time is None:
  130. publish_time = beijing_now()
  131. row = ClientAppVersion(
  132. platform=req.platform.strip().lower(),
  133. channel=req.channel.strip().lower(),
  134. version=req.version.strip(),
  135. build_number=req.build_number,
  136. release_title=(req.release_title or "").strip() or None,
  137. release_notes_html=req.release_notes_html or "",
  138. download_url=(req.download_url or "").strip(),
  139. download_hash=(req.download_hash or "").strip(),
  140. is_force_update=1 if req.is_force_update else 0,
  141. min_supported_version=(req.min_supported_version or "").strip() or None,
  142. status=req.status.strip().lower(),
  143. published_at=publish_time,
  144. created_by=current_admin.username,
  145. updated_by=current_admin.username,
  146. created_at=beijing_now(),
  147. updated_at=beijing_now(),
  148. )
  149. db.add(row)
  150. db.flush()
  151. client_ip = request.client.host if request.client else None
  152. create_operation_log(
  153. db=db,
  154. admin_id=current_admin.id,
  155. module="客户端版本",
  156. action="创建版本",
  157. description=f"创建客户端版本:{row.platform}/{row.channel}/{row.version}",
  158. new_data={"id": row.id, "version": row.version, "status": row.status},
  159. ip_address=client_ip,
  160. )
  161. db.commit()
  162. db.refresh(row)
  163. return {"code": 200, "msg": "创建成功", "data": _format_row(row)}
  164. @router.put("/{version_id}")
  165. async def update_client_version(
  166. version_id: int,
  167. req: ClientVersionUpdate,
  168. request: Request,
  169. db: Session = Depends(get_db),
  170. current_admin=Depends(get_current_admin),
  171. ):
  172. row = db.query(ClientAppVersion).filter(
  173. ClientAppVersion.id == version_id,
  174. ClientAppVersion.is_deleted == 0
  175. ).first()
  176. if not row:
  177. return {"code": 404, "msg": "版本记录不存在"}
  178. old_data = _format_row(row)
  179. payload = req.dict(exclude_unset=True)
  180. if "platform" in payload and payload["platform"] is not None:
  181. row.platform = str(payload["platform"]).strip().lower()
  182. if "channel" in payload and payload["channel"] is not None:
  183. row.channel = str(payload["channel"]).strip().lower()
  184. if "version" in payload and payload["version"] is not None:
  185. row.version = str(payload["version"]).strip()
  186. if "build_number" in payload:
  187. row.build_number = payload["build_number"]
  188. if "release_title" in payload:
  189. row.release_title = (payload["release_title"] or "").strip() or None
  190. if "release_notes_html" in payload:
  191. row.release_notes_html = payload["release_notes_html"] or ""
  192. if "download_url" in payload:
  193. row.download_url = (payload["download_url"] or "").strip()
  194. if "download_hash" in payload:
  195. row.download_hash = (payload["download_hash"] or "").strip()
  196. if "is_force_update" in payload and payload["is_force_update"] is not None:
  197. row.is_force_update = 1 if payload["is_force_update"] else 0
  198. if "min_supported_version" in payload:
  199. row.min_supported_version = (payload["min_supported_version"] or "").strip() or None
  200. if "status" in payload and payload["status"] is not None:
  201. row.status = str(payload["status"]).strip().lower()
  202. if row.status == "published" and not row.published_at:
  203. row.published_at = beijing_now()
  204. if "published_at" in payload:
  205. val = payload["published_at"]
  206. if val:
  207. try:
  208. row.published_at = datetime.fromisoformat(str(val).replace("Z", "+00:00"))
  209. except Exception:
  210. row.published_at = beijing_now()
  211. else:
  212. row.published_at = None
  213. row.updated_by = current_admin.username
  214. row.updated_at = beijing_now()
  215. client_ip = request.client.host if request.client else None
  216. create_operation_log(
  217. db=db,
  218. admin_id=current_admin.id,
  219. module="客户端版本",
  220. action="更新版本",
  221. description=f"更新客户端版本:{row.platform}/{row.channel}/{row.version}",
  222. old_data=old_data,
  223. new_data=_format_row(row),
  224. ip_address=client_ip,
  225. )
  226. db.commit()
  227. return {"code": 200, "msg": "更新成功", "data": _format_row(row)}
  228. @router.delete("/{version_id}")
  229. async def delete_client_version(
  230. version_id: int,
  231. request: Request,
  232. db: Session = Depends(get_db),
  233. current_admin=Depends(get_current_admin),
  234. ):
  235. row = db.query(ClientAppVersion).filter(
  236. ClientAppVersion.id == version_id,
  237. ClientAppVersion.is_deleted == 0
  238. ).first()
  239. if not row:
  240. return {"code": 404, "msg": "版本记录不存在"}
  241. old_data = _format_row(row)
  242. row.is_deleted = 1
  243. row.updated_by = current_admin.username
  244. row.updated_at = beijing_now()
  245. client_ip = request.client.host if request.client else None
  246. create_operation_log(
  247. db=db,
  248. admin_id=current_admin.id,
  249. module="客户端版本",
  250. action="删除版本",
  251. description=f"删除客户端版本:{row.platform}/{row.channel}/{row.version}",
  252. old_data=old_data,
  253. ip_address=client_ip,
  254. )
  255. db.commit()
  256. return {"code": 200, "msg": "删除成功"}