package service import ( "encoding/json" "fmt" "io" "log" "net/http" "os" "os/exec" "path/filepath" "runtime" "strings" "time" "u-desk/internal/common" ) // ==================== 类型定义 ==================== // UpdateService 更新服务 type UpdateService struct { checkURL string // 版本检查接口 URL } // RemoteVersionInfo 远程版本信息 type RemoteVersionInfo struct { Version string `json:"version"` DownloadURL string `json:"download_url"` Changelog string `json:"changelog"` ForceUpdate bool `json:"force_update"` ReleaseDate string `json:"release_date"` FileSize int64 `json:"file_size"` } // UpdateCheckResult 更新检查结果 type UpdateCheckResult struct { HasUpdate bool `json:"has_update"` CurrentVersion string `json:"current_version"` LatestVersion string `json:"latest_version"` DownloadURL string `json:"download_url"` Changelog string `json:"changelog"` ForceUpdate bool `json:"force_update"` ReleaseDate string `json:"release_date"` FileSize int64 `json:"file_size"` } // InstallResult 安装结果 type InstallResult struct { Success bool `json:"success"` Message string `json:"message"` } // ==================== 更新服务 ==================== // NewUpdateService 创建更新服务 func NewUpdateService(checkURL string) *UpdateService { return &UpdateService{ checkURL: checkURL, } } // CheckUpdate 检查更新 func (s *UpdateService) CheckUpdate() (*UpdateCheckResult, error) { log.Printf("[更新检查] 开始检查更新,检查地址: %s", s.checkURL) config, err := LoadUpdateConfig() if err != nil { return nil, fmt.Errorf("加载配置失败: %v", err) } // 同步版本号 currentVersionStr, err := s.syncConfigVersion(config) if err != nil { return nil, err } currentVersion, err := ParseVersion(currentVersionStr) if err != nil { return nil, fmt.Errorf("解析当前版本失败: %v", err) } // 请求远程版本信息 remoteInfo, err := s.fetchRemoteVersionInfo() if err != nil { return nil, fmt.Errorf("获取远程版本信息失败: %v", err) } log.Printf("[更新检查] 远程版本信息: 版本=%s, 下载地址=%s, 强制更新=%v, 更新日志长度=%d", remoteInfo.Version, remoteInfo.DownloadURL, remoteInfo.ForceUpdate, len(remoteInfo.Changelog)) if remoteInfo.Changelog != "" { log.Printf("[更新检查] 更新日志内容: %s", remoteInfo.Changelog) } else { log.Printf("[更新检查] 警告: 远程接口未返回更新日志") } // 解析远程版本号 remoteVersion, err := ParseVersion(remoteInfo.Version) if err != nil { return nil, fmt.Errorf("解析远程版本号失败: %v", err) } // 比较版本 hasUpdate := remoteVersion.IsNewerThan(currentVersion) log.Printf("[更新检查] 版本比较: 当前=%s, 远程=%s, 有更新=%v", currentVersion.String(), remoteVersion.String(), hasUpdate) // 更新最后检查时间 config.UpdateLastCheckTime() result := &UpdateCheckResult{ HasUpdate: hasUpdate, CurrentVersion: currentVersionStr, LatestVersion: remoteInfo.Version, DownloadURL: remoteInfo.DownloadURL, Changelog: remoteInfo.Changelog, ForceUpdate: remoteInfo.ForceUpdate, ReleaseDate: remoteInfo.ReleaseDate, FileSize: remoteInfo.FileSize, } log.Printf("[更新检查] 检查完成: 有更新=%v", hasUpdate) return result, nil } // syncConfigVersion 同步配置中的版本号 func (s *UpdateService) syncConfigVersion(config *UpdateConfig) (string, error) { currentVersionStr := GetCurrentVersion() if currentVersionStr == "" { currentVersionStr = config.CurrentVersion log.Printf("[更新检查] 使用配置中的版本号: %s", currentVersionStr) } else if config.CurrentVersion != currentVersionStr { log.Printf("[更新检查] 配置中的版本号 (%s) 与当前版本号 (%s) 不一致,更新配置", config.CurrentVersion, currentVersionStr) config.CurrentVersion = currentVersionStr if err := SaveUpdateConfig(config); err != nil { log.Printf("[更新检查] 更新配置失败: %v", err) } } return currentVersionStr, nil } // fetchRemoteVersionInfo 获取远程版本信息 func (s *UpdateService) fetchRemoteVersionInfo() (*RemoteVersionInfo, error) { if s.checkURL == "" { log.Printf("[远程版本] 版本检查 URL 未配置") return nil, fmt.Errorf("版本检查 URL 未配置,请先设置检查地址") } log.Printf("[远程版本] 请求远程版本信息: %s", s.checkURL) // 添加时间戳参数防止缓存 timestamp := time.Now().UnixMilli() // 使用毫秒级时间戳 var requestURL string if strings.Contains(s.checkURL, "?") { requestURL = fmt.Sprintf("%s&t=%d", s.checkURL, timestamp) } else { requestURL = fmt.Sprintf("%s?t=%d", s.checkURL, timestamp) } log.Printf("[远程版本] 实际请求URL: %s", requestURL) // 创建 HTTP 客户端,设置超时 client := &http.Client{ Timeout: 10 * time.Second, } // 发送请求 resp, err := client.Get(requestURL) if err != nil { log.Printf("[远程版本] 网络请求失败: %v", err) return nil, fmt.Errorf("网络请求失败: %v", err) } defer resp.Body.Close() log.Printf("[远程版本] HTTP 响应状态码: %d", resp.StatusCode) if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("服务器返回错误状态码: %d", resp.StatusCode) } // 读取响应 body, err := io.ReadAll(resp.Body) if err != nil { log.Printf("[远程版本] 读取响应失败: %v", err) return nil, fmt.Errorf("读取响应失败: %v", err) } log.Printf("[远程版本] 响应内容长度: %d 字节", len(body)) // 解析 JSON var remoteInfo RemoteVersionInfo if err := json.Unmarshal(body, &remoteInfo); err != nil { log.Printf("[远程版本] 解析 JSON 失败: %v, 响应内容: %s", err, string(body)) return nil, fmt.Errorf("解析响应失败: %v", err) } if remoteInfo.Version == "" { log.Printf("[远程版本] 远程版本信息不完整,响应内容: %s", string(body)) return nil, fmt.Errorf("远程版本信息不完整") } log.Printf("[远程版本] 成功获取远程版本信息: %+v", remoteInfo) return &remoteInfo, nil } // ==================== 安装更新 ==================== // InstallUpdate 安装更新包 func InstallUpdate(installerPath string, autoRestart bool) (*InstallResult, error) { return InstallUpdateWithHash(installerPath, autoRestart, "", "") } // InstallUpdateWithHash 安装更新包(带哈希验证) func InstallUpdateWithHash(installerPath string, autoRestart bool, expectedHash string, hashType string) (*InstallResult, error) { if _, err := os.Stat(installerPath); os.IsNotExist(err) { return nil, fmt.Errorf("安装文件不存在: %s", installerPath) } // 哈希验证 if expectedHash != "" && hashType != "" { valid, err := VerifyFileHash(installerPath, expectedHash, hashType) if err != nil { return &InstallResult{Success: false, Message: "文件验证失败: " + err.Error()}, nil } if !valid { return &InstallResult{Success: false, Message: "文件哈希值不匹配,文件可能已损坏或被篡改"}, nil } } // 备份 backupPath, err := BackupApplication() if err != nil { return &InstallResult{Success: false, Message: fmt.Sprintf("备份失败: %v", err)}, nil } // 安装 ext := filepath.Ext(installerPath) switch ext { case ".exe": if runtime.GOOS != "windows" { return &InstallResult{Success: false, Message: "当前系统不是 Windows,无法安装 .exe 文件"}, nil } err = installExe(installerPath) case ".zip": err = installZip(installerPath) default: return nil, fmt.Errorf("不支持的安装包格式: %s", ext) } // 处理安装结果 if err != nil { // 安装失败,尝试回滚 if backupPath != "" { _ = rollbackFromBackup(backupPath) } return &InstallResult{Success: false, Message: fmt.Sprintf("安装失败: %v", err)}, nil } // 自动重启 if autoRestart { go func() { time.Sleep(2 * time.Second) restartApplication() }() } return &InstallResult{Success: true, Message: "安装成功"}, nil } // ==================== 安装相关辅助函数 ==================== // installExe 安装 exe 文件 func installExe(exePath string) error { execPath, err := os.Executable() if err != nil { return err } return replaceExecutableFile(exePath, execPath) } // installZip 安装 ZIP 压缩包 func installZip(zipPath string) error { // 这里需要导入 archive/zip 包 return fmt.Errorf("ZIP 安装暂未实现") } // replaceExecutableFile 替换可执行文件(Windows 和 Unix 通用逻辑) func replaceExecutableFile(newFilePath, execPath string) error { if runtime.GOOS == "windows" { return replaceExecutableFileWindows(newFilePath, execPath) } // Unix-like: 直接替换 if err := copyFile(newFilePath, execPath); err != nil { return fmt.Errorf("复制文件失败: %v", err) } return os.Chmod(execPath, 0755) } // replaceExecutableFileWindows Windows 平台替换可执行文件 func replaceExecutableFileWindows(newFilePath, execPath string) error { oldExecPath := execPath + ".old" newExecPathTemp := execPath + ".new" // 清理旧文件 os.Remove(oldExecPath) os.Remove(newExecPathTemp) // 复制新文件到临时位置 if err := copyFile(newFilePath, newExecPathTemp); err != nil { return fmt.Errorf("复制新文件失败: %v", err) } // 尝试重命名当前文件(如果失败,说明文件正在使用) if err := os.Rename(execPath, oldExecPath); err != nil { return nil } // 替换文件 if err := os.Rename(newExecPathTemp, execPath); err != nil { os.Rename(oldExecPath, execPath) // 恢复 return fmt.Errorf("替换文件失败: %v", err) } // 延迟删除旧文件 go func() { time.Sleep(10 * time.Second) os.Remove(oldExecPath) }() return nil } // restartApplication 重启应用 func restartApplication() { execPath, err := os.Executable() if err != nil { return } currentPID := os.Getpid() if runtime.GOOS != "windows" { return } replacePendingFile(execPath) // 创建并执行重启脚本 tempDir := os.TempDir() batFile := filepath.Join(tempDir, fmt.Sprintf("restart_%d.bat", currentPID)) execDir := filepath.Dir(execPath) batContent := fmt.Sprintf(`@echo off cd /d "%s" start "" "%s" timeout /t 3 /nobreak >nul taskkill /PID %d /F >nul 2>&1 del "%%~f0" `, execDir, execPath, currentPID) if err := os.WriteFile(batFile, []byte(batContent), 0644); err != nil { fallbackRestart(execPath) return } cmd := exec.Command("cmd", "/C", batFile) cmd.Stdout = nil cmd.Stderr = nil if err := cmd.Start(); err != nil { fallbackRestart(execPath) return } os.Exit(0) } // fallbackRestart 降级重启方案 func fallbackRestart(execPath string) { exec.Command("cmd", "/C", "start", "", execPath).Start() time.Sleep(2 * time.Second) os.Exit(0) } // replacePendingFile 替换待替换的文件(.new -> 可执行文件) func replacePendingFile(execPath string) error { newExecPathTemp := execPath + ".new" if _, err := os.Stat(newExecPathTemp); os.IsNotExist(err) { return nil // 没有待替换文件 } oldExecPath := execPath + ".old" os.Remove(oldExecPath) if err := os.Rename(newExecPathTemp, execPath); err != nil { return fmt.Errorf("文件替换失败: %v", err) } return nil } // rollbackFromBackup 从备份恢复 func rollbackFromBackup(backupPath string) error { execPath, err := os.Executable() if err != nil { return err } return copyFile(backupPath, execPath) } // BackupApplication 备份当前应用 func BackupApplication() (string, error) { execPath, err := os.Executable() if err != nil { return "", err } backupDir := filepath.Join(common.GetUserDataDir(), "backups") if err := os.MkdirAll(backupDir, 0755); err != nil { return "", fmt.Errorf("创建备份目录失败: %v", err) } timestamp := time.Now().Format("20060102-150405") backupFileName := fmt.Sprintf("u-desk-backup-%s%s", timestamp, filepath.Ext(execPath)) backupPath := filepath.Join(backupDir, backupFileName) if err := copyFile(execPath, backupPath); err != nil { return "", fmt.Errorf("复制文件失败: %v", err) } return backupPath, nil } // copyFile 复制文件 func copyFile(src, dst string) error { sourceFile, err := os.Open(src) if err != nil { return err } defer sourceFile.Close() destFile, err := os.Create(dst) if err != nil { return err } defer destFile.Close() _, err = destFile.ReadFrom(sourceFile) return err }