如何在NestJS中上传图片并携带其他产品数据?
Angular + NestJS 产品创建与图片上传问题
我需要从Angular客户端向数据库添加产品,后端使用NestJS。产品字段定义如下:
export interface Product { id?: number; name: string; price: number; description: string; category: string; sale: boolean; image: string; genre: string; saleval?: number; }
我用Bootstrap模态框构建了表单:
<div class="modal-body"> <div class="row g-2"> <div class="form-floating col-auto"> <input type="text" class="form-control" placeholder="Name" id="name" [(ngModel)]="name" required> <label class="ms-1" for="name">Name</label> </div> <div class="form-floating col-auto"> <input type="number" class="form-control" placeholder="Price" id="price" [(ngModel)]="price" step="0.01" required> <label class="ms-1" for="price">Price</label> </div> </div> <div class="row g-2 mt-3"> <div class="col-auto"> <input type="text" class="form-control" [(ngModel)]="category" value="Food" placeholder="Food" disabled readonly> </div> <div class="col-auto"> <input type="text" class="form-control" [(ngModel)]="genre" value="fruits" placeholder="Fruits" disabled readonly> </div> </div> <input type="file" accept=".png, .jpeg, .jpg" value="" [(ngModel)]="image" name="file" class="form-control mt-3"> <textarea class="form-control mt-2" cols="30" [(ngModel)]="description" placeholder="Description (max.100 char)" maxlength="100"></textarea> <div class="mt-2"> <label class="me-2" for="sale">On Sale</label> <input type="checkbox" (click)="isSale()"> </div> <div class="" [ngClass]="sale ? 'not-hidden' : 'visually-hidden'"> <input class="form-control" type="number" placeholder="Enter the discount percentage!" min="0" max="100" step="0.01"> </div> </div> <div class="modal-footer"> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> <button type="button" class="btn btn-primary" (click)="postFood()">Add</button> </div>
尝试用常规Post请求实现,代码如下:
Angular组件(food.component.ts)
name: string; price: number; category: string; genre: string; image: string; description: string; sale: boolean = false; saleVal: number; postFood() { if (this.sale == false) { this.saleVal = 0 } if (this.saleVal == 0) { this.sale = false } const product: Product = { name: this.name, price: this.price, description: this.description, category: this.category, sale: this.sale, image: this.image, genre: this.genre, saleval: this.saleVal } this.productsService.postFood(product) }
Angular服务(products.service.ts)
postFood(product: Product) { console.log('Food is added successfully to database!') return this.http.post('http://localhost:3000/food', product) }
NestJS控制器(food.controller.ts)
@Post() @UseInterceptors(FileInterceptor('file')) async create( @Body() product: Product, @UploadedFile() image: Express.Multer.File, ) { product.image = image.buffer.toString() console.log(product) return this.productsService.create(product); } }
NestJS服务(products.service.ts)
async create(product: Product): Promise<any> { return await this.productsRepository.save(product); }
但以上实现报错,我考虑把图片上传拆分到单独接口(如@Post(':id/upload')),但不知道如何获取产品ID进行更新。
问题原因与解决方案
核心问题
你当前的实现错误在于:
- Angular用JSON格式发送请求,但后端
FileInterceptor需要multipart/form-data格式才能解析文件,导致image参数为undefined [(ngModel)]无法绑定文件输入框,无法正确获取文件对象- 表单中
saleVal输入框未绑定双向数据,无法获取折扣值
方案一:单请求完成产品创建+图片上传
修改代码实现multipart/form-data格式提交:
1. 调整Angular表单HTML
<!-- 移除文件输入框的ngModel,添加id用于获取文件 --> <input type="file" accept=".png, .jpeg, .jpg" id="imageInput" name="file" class="form-control mt-3"> <!-- 给saleVal输入框添加双向绑定 --> <div class="" [ngClass]="sale ? 'not-hidden' : 'visually-hidden'"> <input class="form-control" type="number" placeholder="Enter the discount percentage!" min="0" max="100" step="0.01" [(ngModel)]="saleVal"> </div>
2. 修改Angular组件的postFood方法
postFood() { // 处理sale与saleVal的关联逻辑 if (!this.sale) { this.saleVal = 0; } if (this.saleVal === 0) { this.sale = false; } // 创建FormData对象,用于传递表单数据和文件 const formData = new FormData(); // 添加普通字段 formData.append('name', this.name); formData.append('price', this.price.toString()); formData.append('description', this.description); formData.append('category', this.category); formData.append('sale', this.sale.toString()); formData.append('genre', this.genre); formData.append('saleval', this.saleVal?.toString() || '0'); // 获取文件对象并添加到FormData const fileInput = document.getElementById('imageInput') as HTMLInputElement; if (fileInput.files?.length) { formData.append('file', fileInput.files[0]); } // 发送请求并处理结果 this.productsService.postFood(formData).subscribe({ next: (res) => { console.log('产品添加成功', res); // 这里可以添加关闭模态框、重置表单等逻辑 }, error: (err) => { console.error('产品添加失败', err); } }); }
3. 更新Angular服务
postFood(formData: FormData) { return this.http.post('http://localhost:3000/food', formData); }
4. 调整NestJS控制器(适配表单字段类型转换)
创建DTO类用于自动转换字段类型(推荐):
// create-product.dto.ts import { IsString, IsNumber, IsBoolean, IsOptional } from 'class-validator'; import { Transform } from 'class-transformer'; export class CreateProductDto { @IsString() name: string; @IsNumber() @Transform(({ value }) => parseFloat(value)) price: number; @IsString() description: string; @IsString() category: string; @IsBoolean() @Transform(({ value }) => value === 'true') sale: boolean; @IsString() genre: string; @IsOptional() @IsNumber() @Transform(({ value }) => parseFloat(value)) saleval?: number; }
修改控制器:
@Post() @UseInterceptors(FileInterceptor('file')) async create( @Body() productDto: CreateProductDto, @UploadedFile() image: Express.Multer.File, ) { const product: Product = { ...productDto, // 建议转成base64存储,避免buffer.toString()默认utf8导致乱码 image: image ? image.buffer.toString('base64') : '', }; return this.productsService.create(product); }
方案二:分两步上传(先创建产品,再传图片)
如果必须拆分接口,按以下步骤实现:
1. NestJS添加两个接口
// 创建产品(不含图片) @Post() async createProduct(@Body() productDto: CreateProductDto) { const product = await this.productsRepository.save({ ...productDto, image: '', // 初始图片为空 }); return product; // 返回包含id的产品对象 } // 根据ID上传图片 @Post(':id/upload') @UseInterceptors(FileInterceptor('file')) async uploadImage( @Param('id') id: string, @UploadedFile() image: Express.Multer.File, ) { await this.productsRepository.update(+id, { image: image.buffer.toString('base64'), }); return { message: '图片上传成功' }; }
2. Angular服务添加对应方法
createProduct(product: Product) { return this.http.post('http://localhost:3000/food', product); } uploadImage(productId: number, formData: FormData) { return this.http.post(`http://localhost:3000/food/${productId}/upload`, formData); }
3. 修改Angular组件的postFood方法
postFood() { if (!this.sale) { this.saleVal = 0; } if (this.saleVal === 0) { this.sale = false; } const product: Product = { name: this.name, price: this.price, description: this.description, category: this.category, sale: this.sale, genre: this.genre, saleval: this.saleVal, image: '', }; // 第一步:创建产品 this.productsService.createProduct(product).subscribe({ next: (createdProduct) => { // 第二步:上传图片(如果有文件) const fileInput = document.getElementById('imageInput') as HTMLInputElement; if (fileInput.files?.length) { const formData = new FormData(); formData.append('file', fileInput.files[0]); this.productsService.uploadImage(createdProduct.id, formData).subscribe({ next: () => console.log('产品和图片均添加成功'), error: (err) => console.error('图片上传失败', err) }); } else { console.log('产品添加成功,无图片上传'); } }, error: (err) => console.error('产品创建失败', err) }); }
内容的提问来源于stack exchange,提问作者Hazem Kesba
相关产品推荐
相关产品推荐

