3个Bug教你搞定添加产品后端接口,新手避坑实录
刚入行那会儿,从网上扒了个电商项目的 添加产品 接口代码,兴冲冲跑起来,结果全是报错。数据库里没数据,前端传参格式不对,连个 404 错误都看不懂。那种“复制来的代码跑不通不知道怎么调”的无力感,相信每个刚接触后端开发的同学都经历过。今天不聊虚的,我们就以【添加产品】这个最基础但也最容易出错的场景为例,拆解一下大厂面试中关于这一环节的高频考点,顺便把新手常踩的坑一个个填平。
考点梳理:面试官到底在考什么
别以为“添加产品”只是 POST 一个数据那么简单。在面试中,这通常是考察你工程化思维和细节把控能力的试金石。
面试官问“如何实现一个添加产品的功能”,表面是问 CRUD,实际在考察三个层面:
- 数据校验的完整性:你怎么防止非法数据进入数据库?
- 事务的一致性:如果涉及到图片上传、分类关联,中间失败了怎么办?
- 异常处理的优雅性:出错时,用户看到的是堆栈信息还是友好的提示?
很多新手在这里栽跟头,是因为只盯着“怎么插入一条数据”,而忽略了“怎么保证这条数据是干净的、安全的、可追溯的”。
标准答法:结构化表达你的思路
面对这个问题,不要直接甩代码。先口述你的设计思路,这能体现你的逻辑清晰度。建议按以下步骤回答:
- 参数接收与校验:使用框架提供的 DTO(Data Transfer Object)接收前端参数,并进行非空、长度、类型校验。
- 业务逻辑处理:检查分类是否存在、SKU 编码是否唯一等。
- 持久化操作:开启事务,写入数据库。
- 响应反馈:返回统一格式的成功/失败信息。
关键话术:“在实现添加产品时,我会先定义一个 ProductCreateDTO 来承载前端请求,利用 Validation 注解进行前置校验。接着在 Service 层开启事务,确保产品主表和 SKU 子表的数据一致性。最后,通过全局异常处理器捕获潜在错误,返回统一的 Result 对象。”
代码实现:从报错到跑通的实战
下面这段代码是基于 Spring Boot + MyBatis-Plus 的典型实现,也是很多教程里“看起来很美”但实际运行容易出问题的版本。我们来看看怎么改才能真跑通。
// 1. 定义 DTO,注意加上校验注解
@Data
public class ProductCreateDTO {@NotBlank(message = "产品名称不能为空")private String name;@NotNull(message = "价格不能为空")@DecimalMin(value = "0.01", message = "价格必须大于0")private BigDecimal price;@NotNull(message = "分类ID不能为空")private Long categoryId;// 图片列表private List<String> imageUrls;
}// 2. Controller 层
@RestController
@RequestMapping("/api/products")
public class ProductController {@Autowiredprivate ProductService productService;@PostMappingpublic Result<Long> addProduct(@Valid @RequestBody ProductCreateDTO dto) {// @Valid 触发校验,如果失败会抛出 BindException,被全局异常处理器捕获Long id = productService.createProduct(dto);return Result.success(id);}
}// 3. Service 层(核心逻辑)
@Service
public class ProductServiceImpl implements ProductService {@Autowiredprivate ProductMapper productMapper;@Autowiredprivate CategoryMapper categoryMapper;@Override@Transactional(rollbackFor = Exception.class) // 注意:必须回滚所有异常public Long createProduct(ProductCreateDTO dto) {// 1. 校验分类是否存在Category category = categoryMapper.selectById(dto.getCategoryId());if (category == null) {throw new BusinessException("分类不存在");}// 2. 构建实体对象Product product = new Product();product.setName(dto.getName());product.setPrice(dto.getPrice());product.setCategoryId(dto.getCategoryId());// 处理图片,通常存逗号分隔的字符串或 JSON 数组product.setImages(String.join(",", dto.getImageUrls()));// 3. 插入数据库productMapper.insert(product);// 4. 如果有 SKU 逻辑,这里继续插入 SKU// skuService.batchInsertSkus(product.getId(), dto.getSkus());return product.getId();}
}
逐行讲解与避坑点:
- @Valid 的位置:必须加在 Controller 参数前,否则校验不生效。
- @Transactional 的 rollbackFor:默认只回滚 RuntimeException。如果你抛出了
BusinessException这种受检异常,必须显式声明rollbackFor = Exception.class,否则事务不回滚,脏数据就进去了。 - BigDecimal 的使用:价格千万别用 Double,会有精度丢失问题。这是新手最常犯的低级错误。
- 图片存储:MDN Web Docs 中关于 HTML 表单和文件上传的规范提示我们,前端传上来的图片 URL 必须经过后端白名单校验,防止 SSRF 攻击。在实际项目中,这里应该先校验 URL 域名是否在允许的 CDN 列表中。
追问与延伸:面试官的“刁难”时刻
当基础代码写完后,面试官通常会追问几个进阶问题,这也是区分初级和中级的分水岭。
追问1:如果并发请求添加同一个 SKU 编码的产品,怎么办?
- 错误答法:加个 synchronized 锁。
- 正确思路:数据库层面加唯一索引(Unique Index)。在
sku_code字段上建立唯一索引,当并发插入相同编码时,数据库会抛出DuplicateKeyException,捕获这个异常并返回友好提示“SKU已存在”。这是最稳妥的方案,比应用层锁更可靠。
追问2:图片上传和产品保存分离,还是合并在一起?
- 推荐方案:合并在一起,但异步处理。前端先调用上传接口获取图片 URL,再将 URL 列表传给添加产品接口。这样避免了大文件传输阻塞业务接口。
- 进阶:如果图片很大,可以引入 OSS/MinIO,后端只存 Key,通过 CDN 访问。
追问3:怎么防止重复提交?
- 方案:前端禁用按钮 + 后端幂等性控制。可以在 Header 中传递一个 UUID,后端利用 Redis 的
SETNX命令,设置 5 秒过期。如果 Key 已存在,直接拒绝请求。
记忆口诀:三步走,稳准狠
为了在面试中快速组织语言,我总结了一个“三步走”口诀,你可以记下来:
- DTO 接参严校验(非空、类型、业务规则)
- 事务包裹保一致(回滚所有异常、检查关联数据)
- 异常统一友好返(全局捕获、不暴露堆栈、幂等防重)
最后,聊聊一个争议点:在【添加产品】这种写操作中,你更倾向于在 Controller 层做简单的非空检查,还是全部交给 Service 层甚至数据库约束?
有的团队推崇“防御性编程”,认为入口就要挡住脏数据;有的团队则认为“信任框架”,让异常在底层抛出再统一处理。你更常用哪种写法?评论区交流一下你的项目实践,看看哪种方案在你的场景下更稳定。