feat(ghttp): 支持文件上传字段的嵌套结构解析

当前问题,使用嵌套字段时无法自动绑定到嵌套字段的文件 【已解决】

```
type TestData struct {
	ID    int64              `json:"id" dc:"ID"`
	Name  string             `json:"name" dc:"Name"`
	File  *ghttp.UploadFile  `json:"file" dc:"File" type:"file"`
	Files *ghttp.UploadFiles `json:"files" dc:"Files" type:"file"`
}
type TestReq struct {
	g.Meta `path:"/v1/admin/user/test" tags:"AdminUser" method:"POST" summary:"Test"`
	ID     int64             `json:"id" dc:"ID"`
	Data   TestData          `json:"data" dc:"Data"`
	File   *ghttp.UploadFile `json:"file" dc:"File" type:"file"`
}
```

使用 multipart/form-data 上传时 

```
------WebKitFormBoundarypwjjDUNvfZkxxlhH
Content-Disposition: form-data; name="data[id]"

11111111111111112
------WebKitFormBoundarypwjjDUNvfZkxxlhH
Content-Disposition: form-data; name="data[name]"

11111111111111112
------WebKitFormBoundarypwjjDUNvfZkxxlhH
Content-Disposition: form-data; name="data[description]"

11111111111111112
------WebKitFormBoundarypwjjDUNvfZkxxlhH
Content-Disposition: form-data; name="data[file]"; filename="xxxx.jpg"
Content-Type: image/jpeg


------WebKitFormBoundarypwjjDUNvfZkxxlhH
Content-Disposition: form-data; name="file"; filename="xxxxxr.jpg"
Content-Type: image/jpeg


------WebKitFormBoundarypwjjDUNvfZkxxlhH
Content-Disposition: form-data; name="data[files][]"; filename="debug.skk.moe_1732736392647.png"
Content-Type: image/png


------WebKitFormBoundarypwjjDUNvfZkxxlhH
Content-Disposition: form-data; name="data[files][]"; filename="85ee46523adb6a8ee4bf95795c91bef28e24983ed38afbec6789fb5077d75e3f.jpg"
Content-Type: image/jpeg


------WebKitFormBoundarypwjjDUNvfZkxxlhH
Content-Disposition: form-data; name="id"

1000
------WebKitFormBoundarypwjjDUNvfZkxxlhH--
```

【问题描述】
之前使用:
```
	var (
		request = g.RequestFromCtx(ctx)
	)
	var data = request.GetRequestMap()
```
获得的结果是扁平化的:
```
{
  "data": {
    "description": "11111111111111112",
    "id": "11111111111111112",
    "name": "11111111111111112"
  },
  "data[file]": {
    "Filename": "xxxxx.jpg",
  },
  "data[files][]": [
    {
      "Filename": "xxx.png",
    },
    {
      "Filename": "xxx.jpg",
    }
  ],
  "file": {
    "Filename": "xxxx.jpg",
    "Size": 5252553
  },
  "id": "1000"
}
```

由于没有将 `map["data[file]"]` 以 `map["data"]["file"]` 的形式存储,导致最终进行 `r.Parse` 时无法将文件正确绑定到结构体字段:
```
File  *ghttp.UploadFile  `json:"file" dc:"File" type:"file"`
Files *ghttp.UploadFiles `json:"files" dc:"Files" type:"file"`
```
这些字段无论如何都是 nil。

【解决方案】
现在已修复此问题,通过解析嵌套的字段名并构建正确的嵌套Map结构。修复后,`GetRequestMap()` 返回的结果如下:
```
{
  "data": {
    "description": "11111111111111112",
    "id": "11111111111111112",
    "name": "11111111111111112",
    "file": {
      "Filename": "xxxxx.jpg",
    },
    "files[]": [
      {
        "Filename": "xxx.png",
      },
      {
        "Filename": "xxx.jpg",
      }
    ]
  },
  "file": {
    "Filename": "xxxx.jpg",
    "Size": 5252553
  },
  "id": "1000"
}
```

这样,嵌套结构中的文件字段现在可以正确绑定到相应的结构体字段了。这一修复实现了对表单中嵌套文件字段的完整支持。
This commit is contained in:
hinego
2025-04-07 17:53:27 +08:00
committed by GitHub
parent 1534abdb05
commit 51897b6e90

View File

@ -7,6 +7,8 @@
package ghttp
import (
"strings"
"github.com/gogf/gf/v2/container/gvar"
"github.com/gogf/gf/v2/net/goai"
"github.com/gogf/gf/v2/os/gstructs"
@ -113,10 +115,26 @@ func (r *Request) GetRequestMap(kvMap ...map[string]interface{}) map[string]inte
// File uploading.
if r.MultipartForm != nil {
for name := range r.MultipartForm.File {
if uploadFiles := r.GetUploadFiles(name); len(uploadFiles) == 1 {
m[name] = uploadFiles[0]
uploadFiles := r.GetUploadFiles(name)
// 处理嵌套字段名称,如 data[files][]
if strings.Contains(name, "[") && strings.Contains(name, "]") {
// 解析字段名并创建嵌套结构
keys := parseFormNameToKeys(name)
if len(keys) > 0 {
// 使用解析后的键创建嵌套结构
if len(uploadFiles) == 1 {
createNestedMapForFiles(m, keys, uploadFiles[0])
} else {
createNestedMapForFiles(m, keys, uploadFiles)
}
}
} else {
m[name] = uploadFiles
// 常规字段处理,保持原有逻辑
if len(uploadFiles) == 1 {
m[name] = uploadFiles[0]
} else {
m[name] = uploadFiles
}
}
}
}
@ -266,3 +284,78 @@ func mergeTagValueWithFoundKey(data map[string]interface{}, overwritten bool, fi
}
}
}
// parseFormNameToKeys 解析表单字段名称,例如 "data[files][]" 会解析为 ["data", "files[]"]
func parseFormNameToKeys(name string) []string {
// 查找第一个[的位置
firstBracket := strings.Index(name, "[")
if firstBracket < 0 {
return []string{name}
}
// 提取基本名称
base := name[:firstBracket]
keys := []string{base}
// 提取所有括号中的内容
remaining := name[firstBracket:]
for len(remaining) > 0 {
// 找到一对括号
closeBracket := strings.Index(remaining, "]")
if closeBracket < 0 {
break
}
// 提取括号中的内容
key := remaining[1:closeBracket]
// 处理空括号情况 如 []
if len(key) > 0 {
keys = append(keys, key)
} else {
// 对于空括号,将其附加到上一个键
lastIndex := len(keys) - 1
if lastIndex >= 0 {
keys[lastIndex] = keys[lastIndex] + "[]"
}
}
// 继续处理剩余部分
if len(remaining) > closeBracket+1 {
remaining = remaining[closeBracket+1:]
} else {
remaining = ""
}
}
return keys
}
// createNestedMapForFiles 根据解析的键创建嵌套的map结构
func createNestedMapForFiles(m map[string]interface{}, keys []string, value interface{}) {
if len(keys) == 0 {
return
}
// 处理最后一个层级
if len(keys) == 1 {
m[keys[0]] = value
return
}
// 处理中间层级
key := keys[0]
if m[key] == nil {
m[key] = make(map[string]interface{})
}
// 如果当前值不是map则创建一个新的map
subMap, ok := m[key].(map[string]interface{})
if !ok {
subMap = make(map[string]interface{})
m[key] = subMap
}
// 递归处理剩余键
createNestedMapForFiles(subMap, keys[1:], value)
}