后台里接富文本编辑器,最容易误判的一类问题是:图片在页面里能选中,但上传失败;或者本地开发没事,换到测试环境就变成 413、格式不允许、图片地址打不开。
这类问题别只盯前端按钮。富文本上传至少经过五层:编辑器限制、HTTP 请求体限制、后端 multipart 接口、业务上传配置、最终存储目录。哪一层没对上,表现都可能像"编辑器坏了"。
先确认编辑器真正发了什么
以前端 WangEditor 为例,上传图片不是简单把 <input type="file"> 放出来就结束。编辑器里通常会先做类型、数量和大小限制,再把文件按固定字段名提交给后端。
XYGo Admin 的富文本组件里,图片上传默认约束大致是这样:
ts
const uploadConfig = {
server: uploadImageApi,
fieldName: 'file',
maxFileSize: 3 * 1024 * 1024,
maxNumberOfFiles: 10,
allowedFileTypes: ['image/*']
}
排查时先看四件事:字段名是不是 file,Content-Type 是不是 multipart/form-data,实际文件有没有超过前端限制,浏览器 Network 里有没有真正发出上传请求。
如果 Network 里根本没有请求,多半是前端限制挡住了,比如文件类型不在 image/*,或者图片超过 maxFileSize。如果请求发出去了,再继续看后端响应。
后端接口要和字段名对得上
后端最好把上传接口单独收敛出来,不要让富文本、头像、附件各写一套入口。这样日志、权限、大小限制、MIME 检查都比较容易统一。
示例接口可以长这样:
go
type UploadFileReq struct {
g.Meta `path:"/admin/upload/file" method:"post" mime:"multipart/form-data" tags:"文件上传" summary:"上传文件"`
adminin.UploadFileInp
}
type UploadFileInp struct {
Drive string `p:"drive" json:"drive" dc:"可选,覆盖配置中的默认驱动"`
Topic string `p:"topic" json:"topic" dc:"可选,附件分组/主题标识"`
// 文件字段 name=file,由框架从 multipart 读取
}
这里要注意一个细节:前端 fieldName 写的是 file,后端读取 multipart 时也要按这个字段名取。字段名不一致时,接口可能能进 controller,但拿不到文件。很多人会去查对象存储配置,其实问题还停在请求解析这一层。
用 curl 可以快速复现接口,不用每次都从富文本里点:
bash
curl -i \
-H "Authorization: Bearer <token>" \
-F "file=@./demo.png" \
-F "topic=editor" \
http://127.0.0.1:4096/admin/upload/file
正常返回里至少应该能看到访问地址、存储路径、大小、MIME 和扩展名。类似这样的模型就够排障用了:
go
type UploadFileModel struct {
URL string `json:"url"`
Path string `json:"path"`
Size int64 `json:"size"`
Mime string `json:"mime"`
Ext string `json:"ext"`
Drive string `json:"drive"`
AttachmentId uint64 `json:"attachmentId"`
}
如果 curl 成功、富文本失败,回头查前端字段名、鉴权头和跨域。curl 也失败,再看 server 和业务配置。
3MB 和 200MB 不冲突,它们在不同层
上传大小经常让人困惑。前端写了 3MB,服务端配置又写 200MB,是不是互相冲突?通常不是。
可以把限制按层拆开看:
| 层级 | 常见配置 | 作用 |
|---|---|---|
| 编辑器 | maxFileSize: 3 * 1024 * 1024 |
上传前拦截,避免用户选太大的图 |
| Web Server / GoFrame | clientMaxBodySize: 200MB |
请求体上限,超过会直接被服务端拒掉 |
| 业务配置 | upload_max_size |
单文件业务规则,通常比请求体限制更细 |
| 文件类型 | upload_allowed_suffixes、upload_allowed_mime_types |
控制后缀和 MIME |
| 存储目录 | resource/public/attachment/upload |
本地保存位置和静态访问路径 |
GoFrame 配置示例里可以这样写:
yaml
server:
address: ":4096"
clientMaxBodySize: "200MB"
storage:
local:
basePath: "resource/public/attachment/upload"
urlPrefix: "/attachment/upload"
业务配置再从 oss 配置组里读上传驱动、大小、后缀和 MIME:
go
func LoadUploadConfig(ctx context.Context) (*UploadConfig, error) {
cfg, err := service.SysConfig().GetConfigByGroup(ctx, "oss")
if err != nil {
return nil, gerror.Wrap(err, "读取上传配置失败")
}
return &UploadConfig{
Driver: strings.ToLower(firstNonEmpty(cfg["upload_driver"], "local")),
AllowedSuffixes: splitList(cfg["upload_allowed_suffixes"]),
AllowedMimes: splitList(cfg["upload_allowed_mime_types"]),
LocalBaseDir: "resource/public/attachment/upload",
}, nil
}
我的习惯是让 clientMaxBodySize 比单文件限制大一些。它负责挡掉离谱请求;真正的单文件大小、后缀和 MIME 放到业务上传配置里处理。这样以后头像、富文本、附件可以走同一个入口,但规则仍然能分层调整。
本地目录存在,不代表图片一定能访问
本地存储还有一个坑:文件写进目录,只说明保存成功,不代表前端页面能通过 URL 访问。
至少检查这几项:
bash
# 1. 文件有没有落到预期目录
ls -lh resource/public/attachment/upload
# 2. 返回给前端的 URL 是否带了正确前缀
# 例如 /attachment/upload/2026/07/xxx.png
# 3. 静态资源映射是否覆盖这个前缀
curl -I http://127.0.0.1:4096/attachment/upload/2026/07/xxx.png
# 4. 如果前面还有 Nginx,确认没有拦截上传目录
curl -I https://example.com/attachment/upload/2026/07/xxx.png
有些环境里接口返回 200,编辑器里还是显示裂图,最后发现是静态目录没有暴露,或者部署时把 resource/public 放错了工作目录。这类问题和富文本组件关系不大,应该按静态文件访问链路查。
一张排障顺序表
遇到"富文本图片上传失败",我会按这个顺序排:
| 现象 | 优先检查 | 常见原因 |
|---|---|---|
| 选择图片后没有请求 | 浏览器 Network、前端限制 | 文件太大、类型不匹配、编辑器配置没生效 |
| 请求发出但 400/422 | multipart 字段名、后端读取逻辑 | fieldName 和后端字段不一致 |
| 413 或请求直接被拒 | clientMaxBodySize、Nginx 限制 |
请求体上限小于实际上传文件 |
| 接口提示格式不允许 | 后缀、MIME 配置 | 只放开了 png,实际上传 webp/jpg |
| 返回成功但图片裂开 | URL 前缀、静态目录、反代规则 | 文件已保存,但访问路径没有映射 |
| 本地正常,线上失败 | 工作目录、挂载卷、权限 | 上传目录不存在或进程无写权限 |
这也是我觉得可以参考 XYGo Admin 上传链路的原因:它把 WangEditor 富文本上传、/admin/upload/file 接口、上传配置和本地附件目录串在一条线上。看源码时重点翻这几个路径就够了:
web/src/components/core/forms/art-wang-editor/index.vueserver/api/admin/admin_upload.goserver/internal/logic/upload.goserver/internal/model/input/adminin/upload.goserver/manifest/config/config.yaml.example
仓库地址只放一个,方便查上面这些文件: https://github.com/z312193608/xygo-admin
最后提醒一句:别把"前端 3MB、服务端 200MB"这种配置差异直接当 bug。先确定它们分别在哪一层生效,再用 Network、curl 和静态文件访问逐层验证。上传问题看起来杂,按链路拆开后通常不难定位。