1241 lines
37 KiB
Go
1241 lines
37 KiB
Go
package admin
|
|
|
|
import (
|
|
"archive/zip"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
"unicode/utf8"
|
|
|
|
"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"
|
|
"golang.org/x/text/encoding/simplifiedchinese"
|
|
)
|
|
|
|
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)
|
|
versions.POST("/:id/publish", handlePublishVersion)
|
|
versions.POST("/:id/deprecate", handleDeprecateVersion)
|
|
versions.POST("/:id/rollback", handleRollbackVersion)
|
|
}
|
|
}
|
|
|
|
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)
|
|
|
|
var forcedCount int64
|
|
database.DB.Model(&model.Version{}).Where("application_id IN ? AND update_strategy = ?", appIDs, "forced").Count(&forcedCount)
|
|
|
|
// 计算总文件大小
|
|
var totalSize int64
|
|
sizeQuery := database.DB.Model(&model.Version{}).Where("application_id IN ?", appIDs)
|
|
if applicationIDFilter != "" {
|
|
sizeQuery = sizeQuery.Where("application_id = ?", applicationIDFilter)
|
|
}
|
|
if updateStrategyFilter != "" {
|
|
sizeQuery = sizeQuery.Where("update_strategy = ?", updateStrategyFilter)
|
|
}
|
|
if updateTypeFilter != "" {
|
|
sizeQuery = sizeQuery.Where("update_type = ?", updateTypeFilter)
|
|
}
|
|
if updateMethodFilter != "" {
|
|
sizeQuery = sizeQuery.Where("update_method = ?", updateMethodFilter)
|
|
}
|
|
if searchFilter != "" {
|
|
searchLower := strings.ToLower(searchFilter)
|
|
sizeQuery = sizeQuery.Where("LOWER(version) LIKE ? OR LOWER(description) LIKE ?", "%"+searchLower+"%", "%"+searchLower+"%")
|
|
}
|
|
sizeQuery.Select("COALESCE(SUM(file_size), 0)").Scan(&totalSize)
|
|
|
|
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"`
|
|
Status string `json:"status"`
|
|
}
|
|
|
|
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)
|
|
|
|
// 使用数据库中的 publish_status 作为状态
|
|
status := v.PublishStatus
|
|
if status == "" {
|
|
status = "draft"
|
|
}
|
|
|
|
result[i] = VersionWithAppName{
|
|
Version: v,
|
|
ApplicationName: appName,
|
|
Status: status,
|
|
}
|
|
}
|
|
|
|
response.Success(c, gin.H{
|
|
"versions": result,
|
|
"total": total,
|
|
"forced_count": forcedCount,
|
|
"total_size": totalSize,
|
|
})
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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,
|
|
"custom_download_url": version.CustomDownloadURL,
|
|
"changelog": version.Changelog,
|
|
"status": version.PublishStatus,
|
|
"publish_status": version.PublishStatus,
|
|
"superseded_by": version.SupersededBy,
|
|
"published_at": version.PublishedAt,
|
|
"deprecated_at": version.DeprecatedAt,
|
|
"base_version_id": version.BaseVersionID,
|
|
"entry_file": version.EntryFile,
|
|
"entry_files": version.EntryFiles,
|
|
"desktop_shortcut_files": version.DesktopShortcutFiles,
|
|
"full_package_path": version.FullPackagePath,
|
|
"full_package_size": version.FullPackageSize,
|
|
"full_package_hash": version.FullPackageHash,
|
|
"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"`
|
|
EntryFiles string `json:"entry_files"`
|
|
DesktopShortcutFiles string `json:"desktop_shortcut_files"`
|
|
UpdateStrategy string `json:"update_strategy"`
|
|
UpdateType string `json:"update_type"`
|
|
UpdateMethod string `json:"update_method"`
|
|
CustomDownloadURL string `json:"custom_download_url"`
|
|
Description string `json:"description"`
|
|
Changelog string `json:"changelog"`
|
|
BaseVersionID *uint `json:"base_version_id"`
|
|
Files []VersionFileInput `json:"files"`
|
|
}
|
|
|
|
type VersionFileInput 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"`
|
|
IsRequired bool `json:"is_required"`
|
|
}
|
|
|
|
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,
|
|
EntryFiles: req.EntryFiles,
|
|
DesktopShortcutFiles: req.DesktopShortcutFiles,
|
|
UpdateStrategy: req.UpdateStrategy,
|
|
UpdateType: req.UpdateType,
|
|
UpdateMethod: req.UpdateMethod,
|
|
CustomDownloadURL: req.CustomDownloadURL,
|
|
Description: req.Description,
|
|
Changelog: req.Changelog,
|
|
PublishStatus: "draft",
|
|
}
|
|
|
|
if version.UpdateStrategy == "" {
|
|
version.UpdateStrategy = "optional"
|
|
}
|
|
if version.UpdateType == "" {
|
|
version.UpdateType = "full"
|
|
}
|
|
if version.UpdateMethod == "" {
|
|
version.UpdateMethod = "auto"
|
|
}
|
|
|
|
// 处理全量包和补丁包
|
|
if version.UpdateType == "full" {
|
|
// 全量包:full_package 就是自己
|
|
version.FullPackagePath = version.FilePath
|
|
version.FullPackageSize = version.FileSize
|
|
version.FullPackageHash = version.FileHash
|
|
|
|
// 尝试生成增量包(与前一个版本对比)
|
|
var prevVersion model.Version
|
|
if err := database.DB.Where("application_id = ? AND update_type = ?",
|
|
req.ApplicationID, "full").
|
|
Order("id DESC").First(&prevVersion).Error; err == nil {
|
|
// 有前一个版本,生成增量包
|
|
prevFullPackagePath := prevVersion.FullPackagePath
|
|
if prevFullPackagePath == "" {
|
|
prevFullPackagePath = prevVersion.FilePath
|
|
}
|
|
|
|
deltaPath, deltaSize, deltaHash, err := GenerateDeltaPackage(
|
|
prevFullPackagePath,
|
|
version.FilePath,
|
|
req.ApplicationID,
|
|
)
|
|
if err != nil {
|
|
log.Printf("[WARN] Failed to generate delta package: %v\n", err)
|
|
} else if deltaPath != "" {
|
|
version.DeltaPackagePath = deltaPath
|
|
version.DeltaPackageSize = deltaSize
|
|
version.DeltaPackageHash = deltaHash
|
|
log.Printf("[INFO] Delta package created: %s, size: %d\n", deltaPath, deltaSize)
|
|
}
|
|
}
|
|
} else if version.UpdateType == "patch" {
|
|
// 补丁包:需要找到基础全量包并合并
|
|
var baseVersion model.Version
|
|
|
|
// 优先使用传入的 base_version_id
|
|
if req.BaseVersionID != nil {
|
|
if err := database.DB.Where("id = ? AND application_id = ? AND update_type = ?",
|
|
*req.BaseVersionID, req.ApplicationID, "full").First(&baseVersion).Error; err != nil {
|
|
response.Error(c, 400, "指定的基础版本不存在或不是全量版本")
|
|
return
|
|
}
|
|
} else {
|
|
// 未指定基础版本,使用最新的全量版本
|
|
if err := database.DB.Where("application_id = ? AND update_type = ?", req.ApplicationID, "full").
|
|
Order("id DESC").First(&baseVersion).Error; err != nil {
|
|
response.Error(c, 400, "找不到基础全量包,无法创建补丁版本")
|
|
return
|
|
}
|
|
}
|
|
|
|
version.BaseVersionID = &baseVersion.ID
|
|
|
|
// 获取基础版本的全量包路径,如果没有则使用 FilePath
|
|
baseFullPackagePath := baseVersion.FullPackagePath
|
|
if baseFullPackagePath == "" {
|
|
baseFullPackagePath = baseVersion.FilePath
|
|
}
|
|
|
|
// 合并生成新的全量包
|
|
fullPkgPath, fullPkgSize, fullPkgHash, err := MergePatchWithFullPackage(
|
|
baseFullPackagePath,
|
|
req.FilePath,
|
|
req.ApplicationID,
|
|
)
|
|
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
|
|
}
|
|
|
|
if err := database.DB.Create(&version).Error; err != nil {
|
|
response.Error(c, 500, "创建版本失败")
|
|
return
|
|
}
|
|
|
|
// 保存文件列表
|
|
if len(req.Files) > 0 {
|
|
for i, f := range req.Files {
|
|
versionFile := model.VersionFile{
|
|
VersionID: version.ID,
|
|
FilePath: f.FilePath,
|
|
FileName: f.FileName,
|
|
FileSize: f.FileSize,
|
|
FileHash: f.FileHash,
|
|
FileType: f.FileType,
|
|
IsRequired: f.IsRequired,
|
|
SortOrder: i,
|
|
}
|
|
if err := database.DB.Create(&versionFile).Error; err != nil {
|
|
log.Printf("[WARN] Failed to create version file record: %v\n", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
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"`
|
|
FilePath string `json:"file_path"`
|
|
FileSize int64 `json:"file_size"`
|
|
FileHash string `json:"file_hash"`
|
|
EntryFile string `json:"entry_file"`
|
|
EntryFiles string `json:"entry_files"`
|
|
DesktopShortcutFiles string `json:"desktop_shortcut_files"`
|
|
UpdateStrategy string `json:"update_strategy"`
|
|
UpdateType string `json:"update_type"`
|
|
UpdateMethod string `json:"update_method"`
|
|
CustomDownloadURL string `json:"custom_download_url"`
|
|
Description string `json:"description"`
|
|
Changelog string `json:"changelog"`
|
|
BaseVersionID *uint `json:"base_version_id"`
|
|
Files []VersionFileInput `json:"files"`
|
|
}
|
|
|
|
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.FilePath != "" && req.FilePath != version.FilePath {
|
|
updates["entry_file"] = ""
|
|
updates["entry_files"] = ""
|
|
updates["desktop_shortcut_files"] = ""
|
|
} else {
|
|
updates["entry_file"] = req.EntryFile
|
|
updates["entry_files"] = req.EntryFiles
|
|
updates["desktop_shortcut_files"] = req.DesktopShortcutFiles
|
|
}
|
|
|
|
if req.FilePath != "" {
|
|
updates["file_path"] = req.FilePath
|
|
}
|
|
if req.FileSize > 0 {
|
|
updates["file_size"] = req.FileSize
|
|
}
|
|
if req.FileHash != "" {
|
|
updates["file_hash"] = req.FileHash
|
|
}
|
|
if req.UpdateStrategy != "" {
|
|
updates["update_strategy"] = req.UpdateStrategy
|
|
}
|
|
if req.UpdateType != "" {
|
|
updates["update_type"] = req.UpdateType
|
|
}
|
|
if req.UpdateMethod != "" {
|
|
updates["update_method"] = req.UpdateMethod
|
|
}
|
|
updates["custom_download_url"] = req.CustomDownloadURL
|
|
if req.Description != "" {
|
|
updates["description"] = req.Description
|
|
}
|
|
if req.Changelog != "" {
|
|
updates["changelog"] = req.Changelog
|
|
}
|
|
|
|
// 验证 BaseVersionID 不能是自身
|
|
if req.BaseVersionID != nil && *req.BaseVersionID == version.ID {
|
|
response.Error(c, 400, "基础版本不能选择自身")
|
|
return
|
|
}
|
|
|
|
// 处理 UpdateType 变化时的全量包更新
|
|
newUpdateType := req.UpdateType
|
|
if newUpdateType == "" {
|
|
newUpdateType = version.UpdateType
|
|
}
|
|
|
|
// 如果 UpdateType 从 full 改为 patch,需要重新生成全量包
|
|
if version.UpdateType == "full" && newUpdateType == "patch" {
|
|
// 需要有 BaseVersionID
|
|
baseVersionID := req.BaseVersionID
|
|
if baseVersionID == nil {
|
|
baseVersionID = version.BaseVersionID
|
|
}
|
|
if baseVersionID == nil {
|
|
response.Error(c, 400, "补丁包必须指定基础版本")
|
|
return
|
|
}
|
|
|
|
var baseVersion model.Version
|
|
if err := database.DB.First(&baseVersion, *baseVersionID).Error; err != nil {
|
|
response.Error(c, 400, "基础版本不存在")
|
|
return
|
|
}
|
|
|
|
baseFullPackagePath := baseVersion.FullPackagePath
|
|
if baseFullPackagePath == "" {
|
|
baseFullPackagePath = baseVersion.FilePath
|
|
}
|
|
|
|
// 使用当前 FilePath 作为补丁包
|
|
patchPath := req.FilePath
|
|
if patchPath == "" {
|
|
patchPath = version.FilePath
|
|
}
|
|
|
|
fullPkgPath, fullPkgSize, fullPkgHash, err := MergePatchWithFullPackage(
|
|
baseFullPackagePath,
|
|
patchPath,
|
|
version.ApplicationID,
|
|
)
|
|
if err != nil {
|
|
log.Printf("[ERROR] Failed to merge patch with full package: %v\n", err)
|
|
response.Error(c, 500, "合并补丁包失败: "+err.Error())
|
|
return
|
|
}
|
|
|
|
updates["base_version_id"] = baseVersionID
|
|
updates["full_package_path"] = fullPkgPath
|
|
updates["full_package_size"] = fullPkgSize
|
|
updates["full_package_hash"] = fullPkgHash
|
|
log.Printf("[INFO] Regenerated full package for patch type change: %s, size: %d\n", fullPkgPath, fullPkgSize)
|
|
}
|
|
|
|
// 如果 UpdateType 从 patch 改为 full,全量包就是 FilePath 本身
|
|
if version.UpdateType == "patch" && newUpdateType == "full" {
|
|
filePath := req.FilePath
|
|
if filePath == "" {
|
|
filePath = version.FilePath
|
|
}
|
|
fileSize := req.FileSize
|
|
if fileSize == 0 {
|
|
fileSize = version.FileSize
|
|
}
|
|
fileHash := req.FileHash
|
|
if fileHash == "" {
|
|
fileHash = version.FileHash
|
|
}
|
|
|
|
updates["full_package_path"] = filePath
|
|
updates["full_package_size"] = fileSize
|
|
updates["full_package_hash"] = fileHash
|
|
updates["base_version_id"] = nil
|
|
log.Printf("[INFO] Set full package to file itself for full type change: %s\n", filePath)
|
|
}
|
|
|
|
log.Printf("[DEBUG] Updates map: %+v\n", updates)
|
|
|
|
// 如果文件被更新,删除旧文件
|
|
if req.FilePath != "" && req.FilePath != version.FilePath {
|
|
oldFilePath := filepath.Join(".", version.FilePath)
|
|
if _, err := os.Stat(oldFilePath); err == nil {
|
|
if err := os.Remove(oldFilePath); err != nil {
|
|
log.Printf("[WARN] Failed to remove old file %s: %v\n", oldFilePath, err)
|
|
} else {
|
|
log.Printf("[INFO] Removed old file: %s\n", oldFilePath)
|
|
}
|
|
}
|
|
|
|
// 如果是补丁包,需要重新合并生成全量包
|
|
updateType := version.UpdateType
|
|
if req.UpdateType != "" {
|
|
updateType = req.UpdateType
|
|
}
|
|
if updateType == "patch" && version.BaseVersionID != nil {
|
|
var baseVersion model.Version
|
|
if err := database.DB.First(&baseVersion, *version.BaseVersionID).Error; err == nil {
|
|
baseFullPackagePath := baseVersion.FullPackagePath
|
|
if baseFullPackagePath == "" {
|
|
baseFullPackagePath = baseVersion.FilePath
|
|
}
|
|
|
|
fullPkgPath, fullPkgSize, fullPkgHash, err := MergePatchWithFullPackage(
|
|
baseFullPackagePath,
|
|
req.FilePath,
|
|
version.ApplicationID,
|
|
)
|
|
if err != nil {
|
|
log.Printf("[ERROR] Failed to merge patch with full package: %v\n", err)
|
|
response.Error(c, 500, "合并补丁包失败: "+err.Error())
|
|
return
|
|
}
|
|
|
|
updates["full_package_path"] = fullPkgPath
|
|
updates["full_package_size"] = fullPkgSize
|
|
updates["full_package_hash"] = fullPkgHash
|
|
log.Printf("[INFO] Updated full package: %s, size: %d\n", fullPkgPath, fullPkgSize)
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// 更新文件列表
|
|
if len(req.Files) > 0 {
|
|
// 删除旧的文件记录
|
|
database.DB.Where("version_id = ?", version.ID).Delete(&model.VersionFile{})
|
|
// 创建新的文件记录
|
|
for i, f := range req.Files {
|
|
versionFile := model.VersionFile{
|
|
VersionID: version.ID,
|
|
FilePath: f.FilePath,
|
|
FileName: f.FileName,
|
|
FileSize: f.FileSize,
|
|
FileHash: f.FileHash,
|
|
FileType: f.FileType,
|
|
IsRequired: f.IsRequired,
|
|
SortOrder: i,
|
|
}
|
|
if err := database.DB.Create(&versionFile).Error; err != nil {
|
|
log.Printf("[WARN] Failed to create version file record: %v\n", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
}
|
|
|
|
// 检查是否有其他补丁包依赖这些版本
|
|
var dependentPatches []model.Version
|
|
if err := database.DB.Where("base_version_id IN ?", req.IDs).Find(&dependentPatches).Error; err != nil {
|
|
response.Error(c, 500, "检查依赖关系失败")
|
|
return
|
|
}
|
|
if len(dependentPatches) > 0 {
|
|
dependentIDs := make([]string, len(dependentPatches))
|
|
for i, p := range dependentPatches {
|
|
dependentIDs[i] = fmt.Sprintf("v%d.%s", p.ID, p.Version)
|
|
}
|
|
response.Error(c, 400, fmt.Sprintf("以下补丁包依赖所选版本,无法删除: %s", strings.Join(dependentIDs, ", ")))
|
|
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
|
|
}
|
|
|
|
// 恢复被删除版本取代的旧版本为 published
|
|
database.DB.Model(&model.Version{}).
|
|
Where("superseded_by IN ?", req.IDs).
|
|
Updates(map[string]interface{}{
|
|
"publish_status": "published",
|
|
"superseded_by": nil,
|
|
"deprecated_at": nil,
|
|
})
|
|
|
|
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))
|
|
|
|
// 修复中文文件名乱码:如果文件名不是有效 UTF-8,尝试 GBK 解码
|
|
fileName := f.Name
|
|
if !utf8.ValidString(fileName) {
|
|
if decoded, err := simplifiedchinese.GBK.NewDecoder().String(fileName); err == nil {
|
|
fileName = decoded
|
|
}
|
|
}
|
|
|
|
fileType := getFileType(fileName)
|
|
|
|
files = append(files, VersionFileInfo{
|
|
FilePath: fileName,
|
|
FileName: filepath.Base(fileName),
|
|
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"
|
|
}
|
|
}
|
|
|
|
func handlePublishVersion(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))
|
|
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 version.PublishStatus != "draft" && version.PublishStatus != "deprecated" {
|
|
response.Error(c, 400, "只能发布草稿或已废弃的版本")
|
|
return
|
|
}
|
|
|
|
now := time.Now()
|
|
version.PublishStatus = "published"
|
|
version.PublishedAt = &now
|
|
|
|
if err := database.DB.Save(&version).Error; err != nil {
|
|
response.Error(c, 500, "发布版本失败")
|
|
return
|
|
}
|
|
|
|
// 如果是强制更新,标记旧版本为 superseded
|
|
if version.UpdateStrategy == "forced" {
|
|
database.DB.Model(&model.Version{}).
|
|
Where("application_id = ? AND id < ? AND publish_status = ?", version.ApplicationID, version.ID, "published").
|
|
Updates(map[string]interface{}{
|
|
"publish_status": "superseded",
|
|
"superseded_by": version.ID,
|
|
"deprecated_at": now,
|
|
})
|
|
}
|
|
|
|
userIDPtr := &userID
|
|
versionIDPtr := &version.ID
|
|
service.CreateLog(service.LogParams{
|
|
UserID: userIDPtr,
|
|
LogType: "version",
|
|
Action: "publish",
|
|
Resource: "version",
|
|
ResourceID: versionIDPtr,
|
|
Details: fmt.Sprintf("发布版本: %s (应用ID: %d)", version.Version, version.ApplicationID),
|
|
})
|
|
|
|
response.Success(c, gin.H{
|
|
"id": version.ID,
|
|
"publish_status": version.PublishStatus,
|
|
"published_at": version.PublishedAt,
|
|
})
|
|
}
|
|
|
|
func handleDeprecateVersion(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))
|
|
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 version.PublishStatus != "published" {
|
|
response.Error(c, 400, "只能废弃已发布的版本")
|
|
return
|
|
}
|
|
|
|
now := time.Now()
|
|
version.PublishStatus = "deprecated"
|
|
version.DeprecatedAt = &now
|
|
|
|
if err := database.DB.Save(&version).Error; err != nil {
|
|
response.Error(c, 500, "废弃版本失败")
|
|
return
|
|
}
|
|
|
|
// 如果有被该版本取代的版本,恢复它们为 published
|
|
database.DB.Model(&model.Version{}).
|
|
Where("superseded_by = ?", version.ID).
|
|
Updates(map[string]interface{}{
|
|
"publish_status": "published",
|
|
"superseded_by": nil,
|
|
"deprecated_at": nil,
|
|
})
|
|
|
|
userIDPtr := &userID
|
|
versionIDPtr := &version.ID
|
|
service.CreateLog(service.LogParams{
|
|
UserID: userIDPtr,
|
|
LogType: "version",
|
|
Action: "deprecate",
|
|
Resource: "version",
|
|
ResourceID: versionIDPtr,
|
|
Details: fmt.Sprintf("废弃版本: %s (应用ID: %d)", version.Version, version.ApplicationID),
|
|
})
|
|
|
|
response.Success(c, gin.H{
|
|
"id": version.ID,
|
|
"publish_status": version.PublishStatus,
|
|
"deprecated_at": version.DeprecatedAt,
|
|
})
|
|
}
|
|
|
|
func handleRollbackVersion(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))
|
|
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
|
|
}
|
|
|
|
// 只能回滚 superseded 状态的版本
|
|
if version.PublishStatus != "superseded" {
|
|
response.Error(c, 400, "只能回滚已被取代的版本")
|
|
return
|
|
}
|
|
|
|
// 检查取代它的版本是否还存在,如果不存在则直接恢复
|
|
if version.SupersededBy != nil {
|
|
var superseder model.Version
|
|
if err := database.DB.First(&superseder, *version.SupersededBy).Error; err != nil {
|
|
// 取代版本已被删除,直接恢复为 published
|
|
version.PublishStatus = "published"
|
|
version.SupersededBy = nil
|
|
version.DeprecatedAt = nil
|
|
database.DB.Save(&version)
|
|
|
|
userIDPtr := &userID
|
|
versionIDPtr := &version.ID
|
|
service.CreateLog(service.LogParams{
|
|
UserID: userIDPtr,
|
|
LogType: "version",
|
|
Action: "rollback",
|
|
Resource: "version",
|
|
ResourceID: versionIDPtr,
|
|
Details: fmt.Sprintf("回滚版本(取代版本已删除): %s (应用ID: %d)", version.Version, version.ApplicationID),
|
|
})
|
|
|
|
response.Success(c, gin.H{
|
|
"id": version.ID,
|
|
"publish_status": version.PublishStatus,
|
|
})
|
|
return
|
|
}
|
|
}
|
|
|
|
now := time.Now()
|
|
version.PublishStatus = "published"
|
|
version.SupersededBy = nil
|
|
version.DeprecatedAt = nil
|
|
|
|
if err := database.DB.Save(&version).Error; err != nil {
|
|
response.Error(c, 500, "回滚版本失败")
|
|
return
|
|
}
|
|
|
|
// 找到当前发布的版本,将其改为 superseded(如果有的话)
|
|
var currentPublished model.Version
|
|
err := database.DB.Where("application_id = ? AND publish_status = ? AND id != ?",
|
|
version.ApplicationID, "published", version.ID).
|
|
Order("id DESC").First(¤tPublished).Error
|
|
if err == nil {
|
|
currentPublished.PublishStatus = "superseded"
|
|
currentPublished.SupersededBy = &version.ID
|
|
currentPublished.DeprecatedAt = &now
|
|
database.DB.Save(¤tPublished)
|
|
}
|
|
|
|
userIDPtr := &userID
|
|
versionIDPtr := &version.ID
|
|
service.CreateLog(service.LogParams{
|
|
UserID: userIDPtr,
|
|
LogType: "version",
|
|
Action: "rollback",
|
|
Resource: "version",
|
|
ResourceID: versionIDPtr,
|
|
Details: fmt.Sprintf("回滚版本: %s (应用ID: %d)", version.Version, version.ApplicationID),
|
|
})
|
|
|
|
response.Success(c, gin.H{
|
|
"id": version.ID,
|
|
"publish_status": version.PublishStatus,
|
|
})
|
|
}
|