You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Postman中无法读取form-data,结构体数组文件读取失败求助

问题描述

在Postman中无法读取form-data,同时尝试从结构体数组中读取文件时也失败了,以下是我的Go语言代码实现:

type MasterTemplate struct {
    ID       uuid.UUID `form:"id" json:"id"`
    IsVoice  bool      `form:"is_voice" json:"is_voice"`
    Title    string    `form:"title" json:"title"`
    IsSms    bool      `form:"is_sms" json:"is_sms"`
    FilePath string    `form:"file_path" json:"file_path"`
    Content  []struct {
        File             *multipart.FileHeader `form:"file" json:"file"`
        MsgTmplateLangId string                `form:"msg_template_lang_id" json:"msg_template_lang_id"`
        SMSContent       string                `form:"sms_content" json:"sms_content"`
        MsgContentID     string                `form:"msg_content_id" json:"msg_content_id"`
    } `form:"content" json:"content"`
    UserID uuid.UUID `form:"user_id" json:"user_id"`
}

func (tmplController *MessageTemplateController) CreateMsgTemplateController(ctx *gin.Context) {
    userID := ctx.Param("user_id")

    userUUID, err := uuid.Parse(userID)
    if err != nil {
        ctx.JSON(http.StatusBadRequest, gin.H{
            "error": err.Error(),
        })
        ctx.Abort()
        return
    }

    if ctx.Request.ContentLength == 0 {
        ctx.JSON(http.StatusBadRequest, gin.H{
            "error": "Invalid/Empty body request",
        })
        ctx.Abort()
        return
    }

    err = ctx.Request.ParseMultipartForm(10 * 1024 * 1024)
    if err != nil {
        ctx.JSON(http.StatusBadRequest, gin.H{
            "error": err.Error(),
        })
        return
    }

    var msgTemplate *models.MasterTemplate

    if err := ctx.ShouldBind(&msgTemplate); err != nil {
        ctx.JSON(http.StatusBadRequest, gin.H{
            "Gin Error": err.Error(),
        })
        return
    }

    config, err := config.LoadConfig("credentials")
    if err != nil {
        ctx.JSON(http.StatusBadRequest, gin.H{
            "rerror": err.Error(),
        })
        return
    }

    
    for _, sms := range msgTemplate.Content {

        lanaguageID, err := uuid.Parse(sms.MsgTmplateLangId)
        if err != nil {
            ctx.JSON(http.StatusBadRequest, gin.H{
                "rerror": err.Error(),
            })
            return
        }

        templateContent := models.MessageTemplateContenet{
            ID:                uuid.New(),
            LanguageID:        lanaguageID,
            IsSms:             true,
            IsVoice:           false,
            FilePath:          "",
            Content:           sms.SMSContent,
            UserID:            userUUID,
            // MessageTemplateID: data.ID,
            IsDeleted:         false,
        }

        _, err = tmplController.contents.CreateMessageTemplateContent(&templateContent, ctx)
        if err != nil {
            ctx.JSON(http.StatusBadRequest, gin.H{
                "rerror": err.Error(),
            })
            return
        }
    }

    msgTemplate.IsVoice = true
    msgTemplate.IsSms = false

    msgTemplate.IsSms = false
    msgTemplate.IsVoice = true

    for x, voice := range msgTemplate.Content {
        fmt.Println(voice.MsgTmplateLangId)
        fmt.Println(x)
        fileHeader, err := ctx.FormFile(fmt.Sprintf("content[%c][file]", x))
        if err != nil {
            ctx.JSON(http.StatusBadRequest, gin.H{
                "error": err.Error(),
            })
            return
        }
        file, err := voice.File.Open()
        if err != nil {
            ctx.JSON(400, gin.H{"error": "Failed to open uploaded file"})
            return
        }
        defer file.Close()
        fmt.Println(file)

        if !utilities.IsValidFile(fileHeader) {
            ctx.JSON(http.StatusBadRequest, gin.H{
                "error": "Invalid file format. Only .mp3 and .wav files are allowed.",
            })
            return
        }
        if fileHeader.Size > 3*1024*1024 {
            ctx.JSON(http.StatusBadRequest, gin.H{
                "error": "File size exceeds the limit. Maximum file size is 3MB.",
            })
            return
        }
        audioPath := uuid.New().String() + fileHeader.Filename
        uploadPath := filepath.Join("./msgtemplate", audioPath)
        // Save the uploaded file
        if err := ctx.SaveUploadedFile(fileHeader, uploadPath); err != nil {
            ctx.JSON(http.StatusBadRequest, gin.H{
                "error": err.Error(),
            })
            return
        }
        session, err := awsservice.S3BucketSessionHandler()
        if err != nil {
            ctx.JSON(http.StatusBadRequest, gin.H{
                "error": err.Error(),
            })
            return
        }
        fileURL, err := awsservice.UploadFiles(session, uploadPath, config.AWS_AUDIO_BUCKET)
        if err != nil {
            ctx.JSON(http.StatusBadRequest, gin.H{
                "error": err.Error(),
            })
            return
        }
        lanaguageID, err := uuid.Parse(voice.MsgTmplateLangId)
        if err != nil {
            ctx.JSON(http.StatusBadRequest, gin.H{
                "rerror": err.Error(),
            })
            return
        }
        templateContent := models.MessageTemplateContenet{
            ID:         uuid.New(),
            LanguageID: lanaguageID,
            IsSms:      false,
            IsVoice:    true,
            IsDeleted:  false,
            FilePath:   uploadPath,
            Content:    *fileURL,
            UserID:     userUUID,
            // MessageTemplateID: data.ID,
        }

        _, err = tmplController.contents.CreateMessageTemplateContent(&templateContent, ctx)
        if err != nil {
            ctx.JSON(http.StatusBadRequest, gin.H{
                "rerror": err.Error(),
            })
            return
        }

    }
}

解决方案

1. 修正结构体Form标签格式

Gin解析form-data数组时,不需要给Content字段加form:"content"标签,内部字段的标签保持不变即可,Gin会自动识别数组格式的提交键:

type MasterTemplate struct {
    ID       uuid.UUID `form:"id" json:"id"`
    IsVoice  bool      `form:"is_voice" json:"is_voice"`
    Title    string    `form:"title" json:"title"`
    IsSms    bool      `form:"is_sms" json:"is_sms"`
    FilePath string    `form:"file_path" json:"file_path"`
    Content  []struct {
        File             *multipart.FileHeader `form:"file" json:"file"`
        MsgTmplateLangId string                `form:"msg_template_lang_id" json:"msg_template_lang_id"`
        SMSContent       string                `form:"sms_content" json:"sms_content"`
        MsgContentID     string                `form:"msg_content_id" json:"msg_content_id"`
    } `json:"content"` // 移除form标签
    UserID uuid.UUID `form:"user_id" json:"user_id"`
}

2. 初始化结构体实例

原代码中msgTemplate是空指针,Gin无法绑定到空指针,需要初始化结构体:

// 改为值类型初始化
var msgTemplate models.MasterTemplate
// 或者用指针初始化
msgTemplate := new(models.MasterTemplate)

3. 移除手动解析MultipartForm

Gin的ShouldBind方法会自动处理multipart/form-data的解析,手动调用ParseMultipartForm会导致绑定逻辑冲突,直接移除:

// 删掉这行代码
// err = ctx.Request.ParseMultipartForm(10 * 1024 * 1024)

4. 修正数组索引格式化错误

原代码用%c格式化数字索引,会导致键名错误,改为%d:

// 错误写法
// fileHeader, err := ctx.FormFile(fmt.Sprintf("content[%c][file]", x))
// 正确写法
fileHeader, err := ctx.FormFile(fmt.Sprintf("content[%d].file", x))

5. 直接使用绑定后的File字段

ShouldBind成功后,voice.File已经是有效的*multipart.FileHeader,无需再调用ctx.FormFile,直接使用即可:

for x, voice := range msgTemplate.Content {
    if voice.File == nil {
        ctx.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("content[%d].file 不能为空", x)})
        return
    }
    file, err := voice.File.Open()
    if err != nil {
        ctx.JSON(http.StatusBadRequest, gin.H{"error": "Failed to open uploaded file"})
        return
    }
    defer file.Close()

    // 用voice.File代替手动获取的fileHeader
    if !utilities.IsValidFile(voice.File) {
        ctx.JSON(http.StatusBadRequest, gin.H{
            "error": "Invalid file format. Only .mp3 and .wav files are allowed.",
        })
        return
    }
    if voice.File.Size > 3*1024*1024 {
        ctx.JSON(http.StatusBadRequest, gin.H{
            "error": "File size exceeds the limit. Maximum file size is 3MB.",
        })
        return
    }
    audioPath := uuid.New().String() + voice.File.Filename
    uploadPath := filepath.Join("./msgtemplate", audioPath)
    if err := ctx.SaveUploadedFile(voice.File, uploadPath); err != nil {
        ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }
    // 其余逻辑保持不变
}

Postman提交规范

确保form-data的键名遵循以下格式:

  • 顶级字段直接写键名:id、is_voice、title
  • 数组字段按索引命名:content[0].msg_template_lang_id、content[0].file、content[1].msg_template_lang_id、content[1].file

内容的提问来源于stack exchange,提问作者Daniel Tenkorang

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.14 04:55:55