diff --git a/backend/internal/model/models.go b/backend/internal/model/models.go index 3d98fb8..ee2de94 100644 --- a/backend/internal/model/models.go +++ b/backend/internal/model/models.go @@ -220,24 +220,27 @@ type Application struct { // Version 版本模型 type Version struct { - ID uint `gorm:"primaryKey" json:"id"` - ApplicationID uint `json:"application_id"` - Version string `gorm:"size:50" json:"version"` - FilePath string `gorm:"size:255" json:"file_path"` - FileSize int64 `json:"file_size"` - FileHash string `gorm:"size:64" json:"file_hash"` - EntryFile string `gorm:"size:255" json:"entry_file"` - UpdateStrategy string `gorm:"size:20;default:optional" json:"update_strategy"` - UpdateType string `gorm:"size:20;default:full" json:"update_type"` - UpdateMethod string `gorm:"size:20;default:auto" json:"update_method"` - CustomDownloadURL string `gorm:"size:500" json:"custom_download_url"` - Description string `gorm:"type:text" json:"description"` - Changelog string `gorm:"type:text" json:"changelog"` - Status string `gorm:"size:20;default:active" json:"status"` - BaseVersionID *uint `json:"base_version_id"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + ID uint `gorm:"primaryKey" json:"id"` + ApplicationID uint `json:"application_id"` + Version string `gorm:"size:50" json:"version"` + FilePath string `gorm:"size:255" json:"file_path"` + FileSize int64 `json:"file_size"` + FileHash string `gorm:"size:64" json:"file_hash"` + EntryFile string `gorm:"size:255" json:"entry_file"` + UpdateStrategy string `gorm:"size:20;default:optional" json:"update_strategy"` + UpdateType string `gorm:"size:20;default:full" json:"update_type"` + UpdateMethod string `gorm:"size:20;default:auto" json:"update_method"` + CustomDownloadURL string `gorm:"size:500" json:"custom_download_url"` + FullPackagePath string `gorm:"size:255" json:"full_package_path"` + FullPackageSize int64 `json:"full_package_size"` + FullPackageHash string `gorm:"size:64" json:"full_package_hash"` + Description string `gorm:"type:text" json:"description"` + Changelog string `gorm:"type:text" json:"changelog"` + Status string `gorm:"size:20;default:active" json:"status"` + BaseVersionID *uint `json:"base_version_id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` Application Application `gorm:"foreignKey:ApplicationID" json:"application,omitempty"` Files []VersionFile `gorm:"foreignKey:VersionID" json:"files,omitempty"` diff --git a/backend/internal/router/admin/package_util.go b/backend/internal/router/admin/package_util.go new file mode 100644 index 0000000..d8b3d9d --- /dev/null +++ b/backend/internal/router/admin/package_util.go @@ -0,0 +1,155 @@ +package admin + +import ( + "archive/zip" + "fmt" + "io" + "log" + "os" + "path/filepath" + "time" +) + +// MergePatchWithFullPackage 合并补丁包和全量包,生成新的全量包 +func MergePatchWithFullPackage(fullPackagePath, patchPath string, appID, userID uint) (string, int64, string, error) { + // 读取全量包 + fullPackageFile := filepath.Join(".", fullPackagePath) + patchFile := filepath.Join(".", patchPath) + + // 创建临时目录 + tempDir := filepath.Join(os.TempDir(), fmt.Sprintf("merge_%d_%d", appID, time.Now().Unix())) + if err := os.MkdirAll(tempDir, 0755); err != nil { + return "", 0, "", fmt.Errorf("create temp dir failed: %v", err) + } + defer os.RemoveAll(tempDir) + + // 解压全量包 + extractDir := filepath.Join(tempDir, "extracted") + if err := os.MkdirAll(extractDir, 0755); err != nil { + return "", 0, "", fmt.Errorf("create extract dir failed: %v", err) + } + + // 解压全量包 + if err := extractZip(fullPackageFile, extractDir); err != nil { + return "", 0, "", fmt.Errorf("extract full package failed: %v", err) + } + + // 解压补丁包(覆盖同名文件) + if err := extractZip(patchFile, extractDir); err != nil { + return "", 0, "", fmt.Errorf("extract patch failed: %v", err) + } + + // 创建新的全量包 + timestamp := time.Now().Unix() + newFileName := fmt.Sprintf("full_%d_%d_%d.zip", userID, appID, timestamp) + newFilePath := filepath.Join("uploads", "versions", newFileName) + + // 确保目录存在 + if err := os.MkdirAll(filepath.Dir(newFilePath), 0755); err != nil { + return "", 0, "", fmt.Errorf("create dir failed: %v", err) + } + + // 打包新的全量包 + if err := createZipFromDir(extractDir, newFilePath); err != nil { + return "", 0, "", fmt.Errorf("create new full package failed: %v", err) + } + + // 获取文件大小和哈希 + fileInfo, err := os.Stat(newFilePath) + if err != nil { + return "", 0, "", fmt.Errorf("stat new file failed: %v", err) + } + + hash, err := calculateFileHash(newFilePath) + if err != nil { + return "", 0, "", fmt.Errorf("calculate hash failed: %v", err) + } + + log.Printf("[INFO] Created merged full package: %s, size: %d\n", newFilePath, fileInfo.Size()) + + return "/" + filepath.ToSlash(newFilePath), fileInfo.Size(), hash, nil +} + +// extractZip 解压zip文件到指定目录 +func extractZip(zipPath, destDir string) error { + r, err := zip.OpenReader(zipPath) + if err != nil { + return err + } + defer r.Close() + + for _, f := range r.File { + destPath := filepath.Join(destDir, f.Name) + + if f.FileInfo().IsDir() { + os.MkdirAll(destPath, 0755) + continue + } + + // 确保父目录存在 + os.MkdirAll(filepath.Dir(destPath), 0755) + + destFile, err := os.OpenFile(destPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) + if err != nil { + return err + } + + srcFile, err := f.Open() + if err != nil { + destFile.Close() + return err + } + + _, err = io.Copy(destFile, srcFile) + srcFile.Close() + destFile.Close() + + if err != nil { + return err + } + } + + return nil +} + +// createZipFromDir 将目录打包成zip +func createZipFromDir(srcDir, zipPath string) error { + zipFile, err := os.Create(zipPath) + if err != nil { + return err + } + defer zipFile.Close() + + zipWriter := zip.NewWriter(zipFile) + defer zipWriter.Close() + + return filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + relPath, err := filepath.Rel(srcDir, path) + if err != nil { + return err + } + + if info.IsDir() { + _, err = zipWriter.Create(relPath + "/") + return err + } + + writer, err := zipWriter.Create(relPath) + if err != nil { + return err + } + + file, err := os.Open(path) + if err != nil { + return err + } + defer file.Close() + + _, err = io.Copy(writer, file) + return err + }) +} \ No newline at end of file diff --git a/backend/internal/router/admin/versions.go b/backend/internal/router/admin/versions.go index af0808e..029e32c 100644 --- a/backend/internal/router/admin/versions.go +++ b/backend/internal/router/admin/versions.go @@ -336,11 +336,38 @@ func handleCreateVersionGlobal(c *gin.Context) { version.UpdateMethod = "auto" } - if version.UpdateType == "patch" { + // 处理全量包和补丁包 + if version.UpdateType == "full" { + // 全量包:full_package 就是自己 + version.FullPackagePath = version.FilePath + version.FullPackageSize = version.FileSize + version.FullPackageHash = version.FileHash + } else if version.UpdateType == "patch" { + // 补丁包:需要找到基础全量包并合并 var lastFullVersion model.Version if err := database.DB.Where("application_id = ? AND update_type = ?", req.ApplicationID, "full"). Order("id DESC").First(&lastFullVersion).Error; err == nil { version.BaseVersionID = &lastFullVersion.ID + + // 合并生成新的全量包 + fullPkgPath, fullPkgSize, fullPkgHash, err := MergePatchWithFullPackage( + lastFullVersion.FullPackagePath, + req.FilePath, + req.ApplicationID, + userID, + ) + if err != nil { + log.Printf("[ERROR] Failed to merge patch with full package: %v\n", err) + response.Error(c, 500, "合并补丁包失败: "+err.Error()) + return + } + + version.FullPackagePath = fullPkgPath + version.FullPackageSize = fullPkgSize + version.FullPackageHash = fullPkgHash + } else { + response.Error(c, 400, "找不到基础全量包,无法创建补丁版本") + return } } diff --git a/backend/internal/router/app/info.go b/backend/internal/router/app/info.go index 9db1d7b..ce08cac 100644 --- a/backend/internal/router/app/info.go +++ b/backend/internal/router/app/info.go @@ -216,8 +216,18 @@ func handleAppCheckUpdate(c *gin.Context) { } } - // 确定下载地址:优先使用自定义地址 + // 确定下载地址:优先使用完整包地址,其次自定义地址 downloadURL := latestVersion.FilePath + downloadSize := latestVersion.FileSize + downloadHash := latestVersion.FileHash + + // 使用完整包地址(如果有) + if latestVersion.FullPackagePath != "" { + downloadURL = latestVersion.FullPackagePath + downloadSize = latestVersion.FullPackageSize + downloadHash = latestVersion.FullPackageHash + } + // 自定义地址优先级最高 if latestVersion.CustomDownloadURL != "" { downloadURL = latestVersion.CustomDownloadURL } @@ -228,8 +238,8 @@ func handleAppCheckUpdate(c *gin.Context) { "current_version": clientVersion, "latest_version": latestVersion.Version, "download_url": downloadURL, - "file_size": latestVersion.FileSize, - "file_hash": latestVersion.FileHash, + "file_size": downloadSize, + "file_hash": downloadHash, "entry_file": latestVersion.EntryFile, "update_notes": latestVersion.Description, "update_strategy": updateStrategy,