| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161 |
- """
- 客户端版本策略服务
- """
- from __future__ import annotations
- import re
- from typing import Dict, Any, Optional, Tuple
- from sqlalchemy.orm import Session
- from app.models.client_app_version import ClientAppVersion
- from app.models.system_config import SystemConfig
- DEFAULT_PLATFORM = "windows"
- DEFAULT_CHANNEL = "stable"
- DEFAULT_CHECK_INTERVAL_SECONDS = 60
- def _normalize_platform(platform: Optional[str]) -> str:
- value = (platform or "").strip().lower()
- if value in ("windows", "win32", "win", "darwin", "mac", "macos", "linux"):
- if value in ("darwin", "mac"):
- return "macos"
- if value in ("win32", "win"):
- return "windows"
- return value
- return DEFAULT_PLATFORM
- def _normalize_channel(channel: Optional[str]) -> str:
- value = (channel or "").strip().lower()
- return value if value else DEFAULT_CHANNEL
- def _parse_version(version: Optional[str]) -> Optional[Tuple[int, ...]]:
- text = (version or "").strip()
- if not text:
- return None
- parts = [p for p in re.split(r"[.\-+_]", text) if p != ""]
- parsed = []
- for p in parts:
- m = re.match(r"^\d+", p)
- if not m:
- break
- parsed.append(int(m.group(0)))
- if not parsed:
- return None
- return tuple(parsed)
- def _compare_version(a: Optional[str], b: Optional[str]) -> int:
- """
- 比较版本号:
- -1: a < b
- 0: a == b
- 1: a > b
- """
- ta = _parse_version(a)
- tb = _parse_version(b)
- if ta is None and tb is None:
- return 0
- if ta is None:
- return -1
- if tb is None:
- return 1
- max_len = max(len(ta), len(tb))
- aa = list(ta) + [0] * (max_len - len(ta))
- bb = list(tb) + [0] * (max_len - len(tb))
- if aa < bb:
- return -1
- if aa > bb:
- return 1
- return 0
- class ClientVersionService:
- def __init__(self, db: Session):
- self.db = db
- def get_check_interval_seconds(self) -> int:
- row = self.db.query(SystemConfig).filter(
- SystemConfig.config_key == "client_version_check_interval_seconds",
- SystemConfig.app_name.contains("backend-client"),
- SystemConfig.is_deleted == 0
- ).first()
- if not row or not row.config_value:
- return DEFAULT_CHECK_INTERVAL_SECONDS
- try:
- seconds = int(row.config_value)
- return seconds if seconds > 0 else DEFAULT_CHECK_INTERVAL_SECONDS
- except (TypeError, ValueError):
- return DEFAULT_CHECK_INTERVAL_SECONDS
- def get_latest_published(self, platform: Optional[str], channel: Optional[str]) -> Optional[ClientAppVersion]:
- pf = _normalize_platform(platform)
- ch = _normalize_channel(channel)
- rows = self.db.query(ClientAppVersion).filter(
- ClientAppVersion.platform == pf,
- ClientAppVersion.channel == ch,
- ClientAppVersion.status == "published",
- ClientAppVersion.is_deleted == 0
- ).all()
- if not rows:
- return None
- return max(rows, key=lambda r: _parse_version(r.version) or (0,))
- def build_check_result(
- self,
- client_version: Optional[str],
- platform: Optional[str],
- channel: Optional[str],
- ) -> Dict[str, Any]:
- latest = self.get_latest_published(platform, channel)
- interval = self.get_check_interval_seconds()
- if not latest:
- return {
- "latest_version": None,
- "latest_build": None,
- "min_supported_version": None,
- "has_update": False,
- "is_force_update": False,
- "update_title": None,
- "release_notes_html": "",
- "download_url": "",
- "published_at": None,
- "check_interval_seconds": interval,
- "platform": _normalize_platform(platform),
- "channel": _normalize_channel(channel),
- }
- current = (client_version or "").strip()
- latest_version = latest.version
- min_supported = (latest.min_supported_version or "").strip() or latest.version
- has_update = _compare_version(current, latest_version) < 0
- force_by_min = _compare_version(current, min_supported) < 0
- is_force = bool(latest.is_force_update or force_by_min)
- return {
- "latest_version": latest_version,
- "latest_build": latest.build_number,
- "min_supported_version": min_supported,
- "has_update": has_update,
- "is_force_update": bool(is_force and has_update),
- "update_title": latest.release_title or f"发现新版本 {latest_version}",
- "release_notes_html": latest.release_notes_html or "",
- "download_url": latest.download_url or "",
- "published_at": latest.published_at.isoformat() if latest.published_at else None,
- "check_interval_seconds": interval,
- "platform": latest.platform,
- "channel": latest.channel,
- }
- def should_block_client(
- self,
- client_version: Optional[str],
- platform: Optional[str],
- channel: Optional[str],
- ) -> Tuple[bool, Dict[str, Any]]:
- result = self.build_check_result(client_version, platform, channel)
- return bool(result.get("is_force_update")), result
|