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

如何在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进行更新。


问题原因与解决方案

核心问题

你当前的实现错误在于:

  1. Angular用JSON格式发送请求,但后端FileInterceptor需要multipart/form-data格式才能解析文件,导致image参数为undefined
  2. [(ngModel)]无法绑定文件输入框,无法正确获取文件对象
  3. 表单中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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.14 08:37:23