package admin import ( "archive/zip" "crypto/sha256" "encoding/hex" "fmt" "io" "log" "os" "path/filepath" "strconv" "strings" "time" "verification-platform-backend/internal/database" "verification-platform-backend/internal/middleware" "verification-platform-backend/internal/model" "verification-platform-backend/internal/service" "verification-platform-backend/pkg/response" "github.com/gin-gonic/gin" ) func SetupVersionRoutes(r *gin.RouterGroup) { versions := r.Group("/versions") { versions.GET("", handleGetAllVersions) versions.GET("/:id", handleGetVersionByID) versions.POST("", handleCreateVersionGlobal) versions.PUT("/:id", handleUpdateVersionGlobal) versions.DELETE("/batch", handleBatchDeleteVersions) versions.POST("/upload-zip", handleUploadVersionZip) } } func handleGetAllVersions(c *gin.Context) { userID := c.GetUint("user_id") log.Printf("[DEBUG] handleGetAllVersions called, userID: %d\n", userID) page := c.DefaultQuery("page", "1") pageSize := c.DefaultQuery("page_size", "20") applicationIDFilter := c.Query("application_id") updateStrategyFilter := c.Query("update_strategy") updateTypeFilter := c.Query("update_type") updateMethodFilter := c.Query("update_method") searchFilter := c.Query("search") var total int64 var userApps []model.Application if err := database.DB.Where("user_id = ?", userID).Find(&userApps).Error; err != nil { response.Error(c, 500, "获取应用列表失败") return } log.Printf("[DEBUG] Found %d user apps\n", len(userApps)) for i, app := range userApps { log.Printf("[DEBUG] App %d: ID=%d, Name=%s\n", i, app.ID, app.Name) } if len(userApps) == 0 { response.Success(c, gin.H{ "versions": []interface{}{}, "total": 0, }) return } appIDs := make([]uint, len(userApps)) appNameMap := make(map[uint]string) for i, app := range userApps { appIDs[i] = app.ID appNameMap[app.ID] = app.Name } log.Printf("[DEBUG] appNameMap: %v\n", appNameMap) countQuery := database.DB.Model(&model.Version{}).Where("application_id IN ?", appIDs) if applicationIDFilter != "" { countQuery = countQuery.Where("application_id = ?", applicationIDFilter) } if updateStrategyFilter != "" { countQuery = countQuery.Where("update_strategy = ?", updateStrategyFilter) } if updateTypeFilter != "" { countQuery = countQuery.Where("update_type = ?", updateTypeFilter) } if updateMethodFilter != "" { countQuery = countQuery.Where("update_method = ?", updateMethodFilter) } if searchFilter != "" { searchLower := strings.ToLower(searchFilter) countQuery = countQuery.Where("LOWER(version) LIKE ? OR LOWER(description) LIKE ?", "%"+searchLower+"%", "%"+searchLower+"%") } countQuery.Count(&total) query := database.DB.Where("application_id IN ?", appIDs) if applicationIDFilter != "" { query = query.Where("application_id = ?", applicationIDFilter) } if updateStrategyFilter != "" { query = query.Where("update_strategy = ?", updateStrategyFilter) } if updateTypeFilter != "" { query = query.Where("update_type = ?", updateTypeFilter) } if updateMethodFilter != "" { query = query.Where("update_method = ?", updateMethodFilter) } if searchFilter != "" { searchLower := strings.ToLower(searchFilter) query = query.Where("LOWER(version) LIKE ? OR LOWER(description) LIKE ?", "%"+searchLower+"%", "%"+searchLower+"%") } var versions []model.Version offset := 0 if pageInt, err := strconv.Atoi(page); err == nil && pageInt > 1 { offset = (pageInt - 1) * 20 } limit := 20 if pageSizeInt, err := strconv.Atoi(pageSize); err == nil && pageSizeInt > 0 { limit = pageSizeInt } if err := query.Order("created_at DESC").Limit(limit).Offset(offset).Find(&versions).Error; err != nil { response.Error(c, 500, "获取版本列表失败") return } log.Printf("[DEBUG] Found %d versions\n", len(versions)) for i, v := range versions { log.Printf("[DEBUG] Version %d: ID=%d, ApplicationID=%d, Version=%s\n", i, v.ID, v.ApplicationID, v.Version) } versionIDs := make([]uint, len(versions)) versionAppIDMap := make(map[uint]uint) for i, v := range versions { versionIDs[i] = v.ID versionAppIDMap[v.ID] = v.ApplicationID } var allVersionsForApps []model.Version database.DB.Where("application_id IN ?", appIDs).Select("id, application_id, update_strategy").Find(&allVersionsForApps) appForcedVersionIDs := make(map[uint][]uint) for _, v := range allVersionsForApps { if v.UpdateStrategy == "forced" { appForcedVersionIDs[v.ApplicationID] = append(appForcedVersionIDs[v.ApplicationID], v.ID) } } type VersionWithAppName struct { model.Version ApplicationName string `json:"application_name"` } result := make([]VersionWithAppName, len(versions)) for i, v := range versions { appName := appNameMap[v.ApplicationID] log.Printf("[DEBUG] Mapping version %d: ApplicationID=%d -> AppName=%s\n", i, v.ApplicationID, appName) isSuperseded := false if forcedIDs, ok := appForcedVersionIDs[v.ApplicationID]; ok { for _, forcedID := range forcedIDs { if forcedID > v.ID { isSuperseded = true break } } } if isSuperseded { v.Status = "superseded" } else { v.Status = "active" } result[i] = VersionWithAppName{ Version: v, ApplicationName: appName, } } response.Success(c, gin.H{ "versions": result, "total": total, }) } func handleGetVersionByID(c *gin.Context) { userID := c.GetUint("user_id") versionID := c.Param("id") var userApps []model.Application if err := database.DB.Where("user_id = ?", userID).Find(&userApps).Error; err != nil { response.Error(c, 500, "获取应用列表失败") return } appIDs := make([]uint, len(userApps)) appNameMap := make(map[uint]string) for i, app := range userApps { appIDs[i] = app.ID appNameMap[app.ID] = app.Name } var version model.Version if err := database.DB.Where("id = ? AND application_id IN ?", versionID, appIDs).Preload("Files").First(&version).Error; err != nil { response.Error(c, 404, "版本不存在") return } computedStatus := "active" var forcedCount int64 database.DB.Model(&model.Version{}). Where("application_id = ? AND id > ? AND update_strategy = ?", version.ApplicationID, version.ID, "forced"). Count(&forcedCount) if forcedCount > 0 { computedStatus = "superseded" } response.Success(c, gin.H{ "id": version.ID, "application_id": version.ApplicationID, "application_name": appNameMap[version.ApplicationID], "version": version.Version, "description": version.Description, "file_path": version.FilePath, "file_size": version.FileSize, "file_hash": version.FileHash, "update_strategy": version.UpdateStrategy, "update_type": version.UpdateType, "update_method": version.UpdateMethod, "changelog": version.Changelog, "status": computedStatus, "base_version_id": version.BaseVersionID, "files": version.Files, "created_at": version.CreatedAt, "updated_at": version.UpdatedAt, }) } type CreateVersionRequest struct { ApplicationID uint `json:"application_id"` Version string `json:"version"` FilePath string `json:"file_path"` FileSize int64 `json:"file_size"` FileHash string `json:"file_hash"` EntryFile string `json:"entry_file"` UpdateStrategy string `json:"update_strategy"` UpdateType string `json:"update_type"` UpdateMethod string `json:"update_method"` Description string `json:"description"` Changelog string `json:"changelog"` } func handleCreateVersionGlobal(c *gin.Context) { userID := c.GetUint("user_id") var req CreateVersionRequest if err := c.ShouldBindJSON(&req); err != nil { response.Error(c, 400, "参数错误") return } var app model.Application if err := database.DB.Where("id = ? AND user_id = ?", req.ApplicationID, userID).First(&app).Error; err != nil { response.Error(c, 404, "应用不存在") return } if service.GetApplicationDisabledStatus(app.ID) { response.Error(c, 403, "应用已被禁用,无法创建版本") return } var existingVersion model.Version if err := database.DB.Where("application_id = ? AND version = ?", req.ApplicationID, req.Version).First(&existingVersion).Error; err == nil { response.Error(c, 400, "该版本号已存在") return } version := model.Version{ ApplicationID: req.ApplicationID, Version: req.Version, FilePath: req.FilePath, FileSize: req.FileSize, FileHash: req.FileHash, EntryFile: req.EntryFile, UpdateStrategy: req.UpdateStrategy, UpdateType: req.UpdateType, UpdateMethod: req.UpdateMethod, Description: req.Description, Changelog: req.Changelog, Status: "active", } if version.UpdateStrategy == "" { version.UpdateStrategy = "optional" } if version.UpdateType == "" { version.UpdateType = "full" } if version.UpdateMethod == "" { version.UpdateMethod = "auto" } 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 } } if err := database.DB.Create(&version).Error; err != nil { response.Error(c, 500, "创建版本失败") return } userIDPtr := &userID versionIDPtr := &version.ID service.CreateLog(service.LogParams{ UserID: userIDPtr, LogType: "version", Action: "create", Resource: "version", ResourceID: versionIDPtr, Details: fmt.Sprintf("创建版本: %s (应用ID: %d)", version.Version, version.ApplicationID), }) response.Success(c, gin.H{ "id": version.ID, }) } type UpdateVersionRequest struct { Version string `json:"version"` UpdateStrategy string `json:"update_strategy"` UpdateType string `json:"update_type"` UpdateMethod string `json:"update_method"` Description string `json:"description"` Changelog string `json:"changelog"` } func handleUpdateVersionGlobal(c *gin.Context) { userID := c.GetUint("user_id") versionID := c.Param("id") var req UpdateVersionRequest if err := c.ShouldBindJSON(&req); err != nil { log.Printf("[ERROR] ShouldBindJSON failed: %v\n", err) response.Error(c, 400, "参数错误") return } log.Printf("[DEBUG] UpdateVersionRequest parsed: %+v\n", req) var userApps []model.Application if err := database.DB.Where("user_id = ?", userID).Find(&userApps).Error; err != nil { response.Error(c, 500, "获取应用列表失败") return } appIDs := make([]uint, len(userApps)) for i, app := range userApps { appIDs[i] = app.ID } var version model.Version if err := database.DB.Where("id = ? AND application_id IN ?", versionID, appIDs).First(&version).Error; err != nil { response.Error(c, 404, "版本不存在") return } if req.Version != "" && req.Version != version.Version { var existingVersion model.Version if err := database.DB.Where("application_id = ? AND version = ? AND id != ?", version.ApplicationID, req.Version, version.ID).First(&existingVersion).Error; err == nil { response.Error(c, 400, "该版本号已存在") return } } log.Printf("[DEBUG] handleUpdateVersionGlobal versionID=%s, req: version=%s, update_strategy=%s, update_type=%s, update_method=%s, description=%s\n", versionID, req.Version, req.UpdateStrategy, req.UpdateType, req.UpdateMethod, req.Description) updates := map[string]interface{}{} if req.Version != "" { updates["version"] = req.Version } if req.UpdateStrategy != "" { updates["update_strategy"] = req.UpdateStrategy } if req.UpdateType != "" { updates["update_type"] = req.UpdateType } if req.UpdateMethod != "" { updates["update_method"] = req.UpdateMethod } if req.Description != "" { updates["description"] = req.Description } if req.Changelog != "" { updates["changelog"] = req.Changelog } log.Printf("[DEBUG] Updates map: %+v\n", updates) if len(updates) == 0 { response.Success(c, nil) return } result := database.DB.Model(&model.Version{}).Where("id = ?", versionID).Updates(updates) if result.Error != nil { log.Printf("[ERROR] Updates failed: %v\n", result.Error) response.Error(c, 500, "更新版本失败") return } log.Printf("[DEBUG] Updates rows affected: %d\n", result.RowsAffected) if result.RowsAffected == 0 { response.Error(c, 500, "更新未生效,请重试") return } userIDPtr := &userID versionIDPtr := &version.ID service.CreateLog(service.LogParams{ UserID: userIDPtr, LogType: "version", Action: "update", Resource: "version", ResourceID: versionIDPtr, Details: fmt.Sprintf("更新版本: %s (应用ID: %d)", version.Version, version.ApplicationID), }) response.Success(c, nil) } func handleBatchDeleteVersions(c *gin.Context) { userID := c.GetUint("user_id") var req struct { IDs []uint `json:"ids"` } if err := c.ShouldBindJSON(&req); err != nil { response.Error(c, 400, "参数错误") return } if len(req.IDs) == 0 { response.Error(c, 400, "请选择要删除的版本") return } var userApps []model.Application if err := database.DB.Where("user_id = ?", userID).Find(&userApps).Error; err != nil { response.Error(c, 500, "获取应用列表失败") return } appIDs := make([]uint, len(userApps)) for i, app := range userApps { appIDs[i] = app.ID } var versions []model.Version if err := database.DB.Where("id IN ? AND application_id IN ?", req.IDs, appIDs).Find(&versions).Error; err != nil { response.Error(c, 500, "获取版本失败") return } for _, version := range versions { for _, app := range userApps { if app.ID == version.ApplicationID && service.GetApplicationDisabledStatus(app.ID) { response.Error(c, 403, fmt.Sprintf("应用 %s 已被禁用,无法删除其版本", app.Name)) return } } } if err := database.DB.Where("id IN ? AND application_id IN ?", req.IDs, appIDs).Delete(&model.Version{}).Error; err != nil { response.Error(c, 500, "批量删除版本失败") return } response.Success(c, nil) } type UploadVersionZipResponse struct { FilePath string `json:"file_path"` FileSize int64 `json:"file_size"` FileHash string `json:"file_hash"` Files []VersionFileInfo `json:"files"` } type VersionFileInfo struct { FilePath string `json:"file_path"` FileName string `json:"file_name"` FileSize int64 `json:"file_size"` FileHash string `json:"file_hash"` FileType string `json:"file_type"` } func handleUploadVersionZip(c *gin.Context) { userID := c.GetUint("user_id") file, header, err := c.Request.FormFile("file") if err != nil { response.Error(c, 400, "请上传ZIP文件") return } defer file.Close() if !strings.HasSuffix(strings.ToLower(header.Filename), ".zip") { response.Error(c, 400, "只支持ZIP格式文件") return } fileSize := header.Size var user model.User if err := database.DB.First(&user, userID).Error; err != nil { response.Error(c, 500, "获取用户信息失败") return } if user.CurrentPackageID != nil { var permission model.PackagePermission if err := database.DB.Where("package_id = ?", user.CurrentPackageID).First(&permission).Error; err == nil { maxStorageBytes := int64(permission.MaxStorage) * 1024 * 1024 if user.StorageUsed+fileSize > maxStorageBytes { usedMB := float64(user.StorageUsed) / 1024 / 1024 maxMB := float64(permission.MaxStorage) response.Error(c, 403, fmt.Sprintf("存储空间不足,已使用 %.2f MB / %.2f MB", usedMB, maxMB)) return } } } timestamp := time.Now().Unix() filename := fmt.Sprintf("version_%d_%d.zip", userID, timestamp) dst := filepath.Join("uploads", "versions", filename) if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil { response.Error(c, 500, "创建目录失败") return } dstFile, err := os.Create(dst) if err != nil { response.Error(c, 500, "创建文件失败") return } defer dstFile.Close() if _, err := io.Copy(dstFile, file); err != nil { response.Error(c, 500, "保存文件失败") return } zipHash, err := calculateFileHash(dst) if err != nil { os.Remove(dst) response.Error(c, 500, "计算文件哈希失败") return } files, err := parseZipFile(dst) if err != nil { os.Remove(dst) response.Error(c, 500, fmt.Sprintf("解析ZIP文件失败: %v", err)) return } if err := middleware.UpdateStorageUsed(userID, fileSize, "upload"); err != nil { log.Printf("更新存储使用量失败: %v\n", err) } usage := model.StorageUsage{ UserID: userID, ResourceType: "version", ResourceID: 0, FileName: header.Filename, FileSize: fileSize, Action: "upload", CreatedAt: time.Now(), } database.DB.Create(&usage) response.Success(c, UploadVersionZipResponse{ FilePath: fmt.Sprintf("/uploads/versions/%s", filename), FileSize: fileSize, FileHash: zipHash, Files: files, }) } func calculateFileHash(filePath string) (string, error) { file, err := os.Open(filePath) if err != nil { return "", err } defer file.Close() hash := sha256.New() if _, err := io.Copy(hash, file); err != nil { return "", err } return hex.EncodeToString(hash.Sum(nil)), nil } func parseZipFile(zipPath string) ([]VersionFileInfo, error) { reader, err := zip.OpenReader(zipPath) if err != nil { return nil, err } defer reader.Close() var files []VersionFileInfo for _, f := range reader.File { if f.FileInfo().IsDir() { continue } rc, err := f.Open() if err != nil { continue } hash := sha256.New() if _, err := io.Copy(hash, rc); err != nil { rc.Close() continue } rc.Close() fileHash := hex.EncodeToString(hash.Sum(nil)) fileType := getFileType(f.Name) files = append(files, VersionFileInfo{ FilePath: f.Name, FileName: filepath.Base(f.Name), FileSize: int64(f.UncompressedSize64), FileHash: fileHash, FileType: fileType, }) } return files, nil } func getFileType(filename string) string { ext := strings.ToLower(filepath.Ext(filename)) switch ext { case ".exe", ".dll", ".so", ".dylib", ".app": return "executable" case ".json", ".xml", ".yaml", ".yml", ".ini", ".conf", ".cfg": return "config" case ".txt", ".md", ".log": return "text" case ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".ico", ".svg": return "image" case ".mp3", ".wav", ".ogg", ".flac": return "audio" case ".mp4", ".avi", ".mkv", ".mov", ".wmv": return "video" case ".db", ".sqlite", ".sqlite3": return "database" default: return "resource" } }