Initial commit: 网络验证平台
This commit is contained in:
@@ -0,0 +1,565 @@
|
||||
package developer
|
||||
|
||||
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")
|
||||
|
||||
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)
|
||||
|
||||
database.DB.Model(&model.Version{}).Where("application_id IN ?", appIDs).Count(&total)
|
||||
|
||||
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 := database.DB.Where("application_id IN ?", appIDs).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)
|
||||
}
|
||||
|
||||
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)
|
||||
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
|
||||
}
|
||||
|
||||
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,
|
||||
"force_update": version.ForceUpdate,
|
||||
"update_strategy": version.UpdateStrategy,
|
||||
"update_method": version.UpdateMethod,
|
||||
"min_version": version.MinVersion,
|
||||
"changelog": version.Changelog,
|
||||
"status": version.Status,
|
||||
"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"`
|
||||
ForceUpdate bool `json:"force_update"`
|
||||
UpdateStrategy string `json:"update_strategy"`
|
||||
UpdateMethod string `json:"update_method"`
|
||||
MinVersion string `json:"min_version"`
|
||||
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,
|
||||
ForceUpdate: req.ForceUpdate,
|
||||
UpdateStrategy: req.UpdateStrategy,
|
||||
UpdateMethod: req.UpdateMethod,
|
||||
MinVersion: req.MinVersion,
|
||||
Description: req.Description,
|
||||
Changelog: req.Changelog,
|
||||
Status: "active",
|
||||
}
|
||||
|
||||
if version.UpdateStrategy == "" {
|
||||
version.UpdateStrategy = "optional"
|
||||
}
|
||||
if version.UpdateMethod == "" {
|
||||
version.UpdateMethod = "manual"
|
||||
}
|
||||
|
||||
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"`
|
||||
ForceUpdate bool `json:"force_update"`
|
||||
UpdateStrategy string `json:"update_strategy"`
|
||||
UpdateMethod string `json:"update_method"`
|
||||
MinVersion string `json:"min_version"`
|
||||
Description string `json:"description"`
|
||||
Changelog string `json:"changelog"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
func handleUpdateVersionGlobal(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
versionID := c.Param("id")
|
||||
|
||||
var req UpdateVersionRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
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 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
|
||||
}
|
||||
version.Version = req.Version
|
||||
}
|
||||
|
||||
if req.Version != "" {
|
||||
version.Version = req.Version
|
||||
}
|
||||
version.ForceUpdate = req.ForceUpdate
|
||||
if req.UpdateStrategy != "" {
|
||||
version.UpdateStrategy = req.UpdateStrategy
|
||||
}
|
||||
if req.UpdateMethod != "" {
|
||||
version.UpdateMethod = req.UpdateMethod
|
||||
}
|
||||
version.MinVersion = req.MinVersion
|
||||
version.Description = req.Description
|
||||
version.Changelog = req.Changelog
|
||||
if req.Status != "" {
|
||||
version.Status = req.Status
|
||||
}
|
||||
|
||||
if err := database.DB.Save(&version).Error; err != nil {
|
||||
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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user