Strapi 文件上传与模型创建同步:完整解决方案
2025-03-11 02:07:20
Strapi: 如何同时上传文件并创建新模型条目?
搞开发的时候,经常需要在 Strapi 里面一次性做两件事:上传文件,然后创建一个新的数据条目,把文件跟数据条目关联起来。Strapi 的后台管理面板能这么干,那咱们自己的应用里当然也得行。问题就出在,创建新条目之前,你是不知道它的 ID 的 (RefID),所以直接把文件和还没影儿的数据条目关联上,这事儿有点麻烦。我试了好几种办法,下面给大家捋捋,哪些能成,哪些是坑。
问题根源在哪儿?
Strapi 的数据创建和文件上传是分开处理的。 你往 /jobs
发个 POST 请求创建数据,文件上传则需要另一个接口,一般是 /upload
。 直接把文件和其它数据揉到一个 FormData 里发过去,Strapi 就有点懵,不知道怎么处理了。所以经常会碰到报错,比如“table jobs has no column named fields”,或者就是干脆文件上传失败.
靠谱的解决方案
好,咱们来看看解决这个问题的几个法子。
方法一:两步走,先创建条目,再上传文件
这是最稳妥的方法, 分成两个步骤:
-
先创建数据条目: 只发送除文件外的其他数据到
/jobs
接口创建条目。创建成功后,你会从响应里拿到新条目的 ID。 -
再上传文件并关联: 利用上一步拿到的 ID,通过
/upload
接口上传文件。上传的时候,通过ref
、refId
和field
这几个参数,把文件和刚创建的数据条目关联上。
代码示例:
先调整你的 createJob
服务:
// services/job.service.ts
public createJobWithoutFiles = (payload: IJob): Observable<IJob> => {
// 注意这里只发送纯数据,不包含 files 字段
const { files, ...jobData } = payload; // 把 files 字段排除掉
return this.http.post<IJob>(`${this.env.backendUrl}/jobs`, jobData);
}
public uploadFilesAndLink = (jobId: string, files: File[], fieldName: string): Observable<any> => {
const data = new FormData();
files.forEach(file => {
data.append('files', file);
});
data.append('ref', 'job'); // 你的模型名称
data.append('refId', jobId); // 新创建的 job 的 ID
data.append('field', fieldName); // job 模型里对应的文件字段名,比如 'files'
return this.http.post(`${this.env.backendUrl}/upload`, data);
}
然后在你的 action 或组件里,把这两个步骤串起来:
// 在你的 effect 或者 component 里面.
this.jobService.createJobWithoutFiles(jobsForm.value)
.pipe(
switchMap(createdJob => {
const jobId = createdJob.id; // 从返回的结果里拿到 ID
const files: File[] = jobsForm.value.files; //从表单获取 File 对象
return this.jobService.uploadFilesAndLink(jobId, files, 'files'); //假设字段名为 'files'
})
)
.subscribe(
() => {
//两个操作都搞定了! 文件已上传,关联也没问题!
console.log("Job created and Files Upload Success!");
},
err => {
// 处理各种错误
console.log("出错了:",err);
}
);
安全提示:
- 确保上传接口做了适当的权限控制,只允许授权用户上传文件。
- 对上传的文件类型和大小进行限制,防止恶意文件上传。
方法二: 使用 Strapi 的 content-type: multipart/form-data
(Strapi v4+)
从 Strapi v4 开始,可以直接使用 multipart/form-data
的请求一次性完成条目创建和文件上传,但是需要特殊的处理。
- 构建 FormData: 数据结构需要调整,把普通字段放到一个叫
data
的 JSON 字符串里,文件还是放在files.<fieldName>
里.
代码示例:
// service
public createJobWithFiles = (payload: IJob): Observable<IJob> => {
const data = new FormData();
// 把除 files 之外的字段都放到 data 里,并转成 JSON 字符串
const { files, ...jobData } = payload;
data.append('data', JSON.stringify(jobData));
// 处理文件字段, 把每个文件都加进去
if (files) {
for (let i = 0; i < files.length; i++) {
data.append(`files.files`, files[i]); // 假设字段名叫 files
}
}
return this.http.post<IJob>(`${this.env.backendUrl}/jobs`, data);
}
组件或 Action 里调用:
this.jobService.createJobWithFiles(this.jobsForm.value).subscribe(
() => console.log('搞定!数据和文件一次性创建上传!'),
(err) => console.error("哎呀,报错了",err)
);
关键点解释:
data
字段: 包含除了文件之外的所有数据。要把它转成 JSON 字符串。files.<fieldName>
:fieldName
替换成你的模型里文件字段的名称 (比如files.myFiles
,files.documents
)。可以一次上传多个文件到同一个字段。- 如果你用了更复杂的结构,例如你的files字段在一个叫做
job_details
的组件里, 你的files.<fieldName>
会变成files.job_details.files
.
安全提示:
和方法一类似,要做足文件校验和权限管理.
进阶技巧: 自定义 Controller 增强控制 (可选)
如果你需要对整个过程做更细粒度的控制, 比如更复杂的关联逻辑, 文件预处理, 或者整合第三方服务,可以自定义 Strapi 的 Controller。
-
创建自定义 Controller:
在src/api/job/controllers
下,创建或修改文件job.js
:// src/api/job/controllers/job.js 'use strict'; const { createCoreController } = require('@strapi/strapi').factories; module.exports = createCoreController('api::job.job', ({ strapi }) => ({ async createWithFiles(ctx) { try { let { data, files } = ctx.request.body; data = JSON.parse(data); //解析 'data' // 使用 Strapi 的 entityService 来创建条目. const newJob = await strapi.entityService.create('api::job.job', { data, }); // 如果有文件,处理文件上传. if (files && Object.keys(files).length > 0) { // 循环 files 对象, 使用 Strapi 内部的 upload service上传并关联. for (const fileKey in files) { await strapi.plugins.upload.services.upload.upload({ data: { refId: newJob.id, ref: 'api::job.job', //必须是api::<name>.<name>格式 field: fileKey.split('.').pop(),//获取字段名称 }, files: files[fileKey] }); } } return newJob; } catch(err) { ctx.throw(500, err); console.log(err) } } }));
-
配置路由:
在src/api/job/routes/job.js
(如果需要自定义, 创建或者修改此文件)
// src/api/job/routes/job.js
'use strict';
module.exports = {
routes: [
{
method: 'POST',
path: '/jobs/create-with-files', // 你自定义的路由
handler: 'job.createWithFiles', // 指向你自定义的 controller 和方法
config: {
policies: [], // 如果需要,可以添加策略
middlewares: []
},
},
],
};
- 修改前端服务以使用新路由:
现在你的前端服务应该调用/jobs/create-with-files
, 而不是原来的/jobs
public createJobWithFilesCustom = (payload: IJob): Observable<IJob> => {
const data = new FormData();
const { files, ...jobData } = payload;
data.append('data', JSON.stringify(jobData));
if(files) {
for( let i = 0; i < files.length; i++ ) {
data.append(`files.files`, files[i]); //假设字段名叫 files
}
}
return this.http.post<IJob>(`${this.env.backendUrl}/jobs/create-with-files`, data);
}
注意
- api::
. 这种格式是 Strapi 内部使用的. - 上述示例中, 我们用到了
strapi.plugins.upload.services.upload.upload
方法, 这属于Strapi内部API,将来Strapi版本更新有变动的可能. - 使用自定义 Controller 之后, 相当于完全接管了处理流程, Strapi 自带的一些功能(比如数据验证), 可能需要你手动去实现。
总结一下
通常情况下,方法二 (直接用 multipart/form-data
) 就够用了,简单直接。 如果你有特殊的业务逻辑需求,再考虑自定义 Controller。