Java-springboot生鲜电商项目(四)商品模块
- 商品模块
- 涉及到的接口
- 这个模块的难点
- 常见错误
- (一)后台的新增商品
- 在request创建AddProduct类,目的是为了修改product不会污染pojo下的product类
- 在productMapper创建查询商品名的接口,用于之后的重名判断
- 在productMapper.xml编写SQL语句
- 在productServiceImpl实现商品插入的业务逻辑
- 写到这里,会出现一个问题,就是图片上传问题,处理步骤如下:
- UUID
- 在Constant常量类中编写图片文件保存的地址并在类上加入`@Component`注解
- 在配置文件是配置本地电脑保存图片文件的路径,我在这里踩了一个坑,我在文件夹后面少了一个"/"导致postman测试不到,一直报20000系统错误
- 在MallExceptionEnum中加入
- 在productController中编写增加商品和图片上传的接口
- 在postman中进行商品添加和添加图片的操作
- 测试都没毛病,但是,我把图片地址信息放在浏览器上,会出现报错信息,打不开图片:原因是关于自定义静态目录资源映射目录这个点
- 遇到这个问题需要在config下的MallWebMvcConfig进行配置地址映射,添加上
- (二)后台的更新商品接口
- 在request新增UpdateProduct
- 在ProductServiceImpl中创建更新商品的业务逻辑
- 在Productcontroller实现更新操作
- (三)后台的删除商品接口实现
- 在productServiceImpl根据传入的ID进行商品的删除
- 在ProductController中实现删除商品
- 使用postman进行接口的测试
- (四)后台的批量上下架(难点)
- dao层接口编写
- mapper.xml SQL编写
- serviceImple层接口编写
- controller层接口编写
- postman测试接口
- (五)前台商品列表接口开发
- dao
- mapper
- serviceImpl
- controller
- (六)前台商品详情接口开发
- serviceImpl,直接复用之前的业务逻辑,不用重新从底层编写
- controller
- (七)前台商品列表接口(非常难)
- 具体功能实现
- 前台商品列表的类
- 因为目录处理:如果查询某个目录下的商品,不仅需要查出来该目录,还需要查询处理子目录的所有商品。所有再新建一个quey包,新建ProductListQuery类,包含商品列表集合
- 因为用户查询的目录不是所有的目录,所以在之前编写的categoryService.listCategoryForCustomer需要进行重构:将parentId变成是用户自己传进来
- 改动完后,还需要对service层和controller层进行同步的改动
- 排序处理
- 在constant包中创建支持排序的规则
- dao
- mapper
- service
- controller
- postman
商品模块
涉及到的接口
- 增加商品
- 上传图片
- 更新商品
- 删除商品
- 批量上下架商品
- 商品列表(后台)
- 前台:商品列表
- 商品详情
这个模块的难点
- 商品搜索
- 排序
- 目录查询
常见错误
- 更新和新增放在同一个接口
- 排序字段不用枚举
(一)后台的新增商品
在request创建AddProduct类,目的是为了修改product不会污染pojo下的product类
package com.hyb.mall.model.request;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import java.util.Date;
public class AddProductReq {
@NotNull(message = "商品名称不能为空")
private String name;
@NotNull(message = "商品图片不能为空")
private String image;
private String detail;
@NotNull(message = "商品分类不能为空")
private Integer categoryId;
@NotNull(message = "商品价格不能为空")
@Min(value = 1, message = "价格不能小于1分钱")
private Integer price;
@NotNull(message = "商品库存不能为空")
@Max(value = 10000, message = "库存不能大于1万")
private Integer stock;
private Integer status;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name == null ? null : name.trim();
}
public String getImage() {
return image;
}
public void setImage(String image) {
this.image = image == null ? null : image.trim();
}
public String getDetail() {
return detail;
}
public void setDetail(String detail) {
this.detail = detail == null ? null : detail.trim();
}
public Integer getCategoryId() {
return categoryId;
}
public void setCategoryId(Integer categoryId) {
this.categoryId = categoryId;
}
public Integer getPrice() {
return price;
}
public void setPrice(Integer price) {
this.price = price;
}
public Integer getStock() {
return stock;
}
public void setStock(Integer stock) {
this.stock = stock;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
}
在productMapper创建查询商品名的接口,用于之后的重名判断
Product selectByName(String name);
在productMapper.xml编写SQL语句
<select id="selectByName" parameterType="java.lang.String" resultMap="BaseResultMap">
select
<include refid="Base_Column_List"/>
from imooc_mall_product
where name=#{name,jdbcType=VARCHAR}
</select>
在productServiceImpl实现商品插入的业务逻辑
package com.hyb.mall.service.impl;
import com.hyb.mall.exception.MallException;
import com.hyb.mall.exception.MallExceptionEnum;
import com.hyb.mall.model.dao.ProductMapper;
import com.hyb.mall.model.pojo.Product;
import com.hyb.mall.model.request.AddProductReq;
import com.hyb.mall.service.ProductService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class ProductServiceImpl implements ProductService {
@Autowired
ProductMapper productMapper;
@Override
public void add(AddProductReq addProductReq) {
//1.创建product实例
Product product = new Product();
//2.将新增的product复制到pojo中product类中,对数据进行一次覆盖
BeanUtils.copyProperties(addProductReq, product);
//3.通过查询数据库是否有重名的商品,有就添加失败
Product productOld = productMapper.selectByName(addProductReq.getName());
if (productOld != null) {
throw new MallException(MallExceptionEnum.NAME_EXISTED);
}
//4.进行插入数据操作并判断是否有效插入
int count = productMapper.insertSelective(product);
if (count == 0) {
throw new MallException(MallExceptionEnum.CREATE_FAILE);
}
}
}
写到这里,会出现一个问题,就是图片上传问题,处理步骤如下:
UUID
- 使用UUID(通用唯一识别码)在本地上传,风险不大,在服务器上传图片,如果有重名就会将原来的图片覆盖掉,也是防止爬图
- UUID的生成规则:日期和时间,Mac地址,hashcode,随机数
在Constant常量类中编写图片文件保存的地址并在类上加入@Component
注解
//因为存在静态的static,用普通的方式进行处理,是注入不进去的 在进行图片上传的时候回报错
public static String FILE_UPLOAD_DIR;
@Value("${file.upload.dir}")
public void setFileUploadDir(String fileUploadDir){
FILE_UPLOAD_DIR=fileUploadDir;
}
在配置文件是配置本地电脑保存图片文件的路径,我在这里踩了一个坑,我在文件夹后面少了一个"/"导致postman测试不到,一直报20000系统错误
file.upload.dir=/Users/hyb/Desktop/Pfile/
在MallExceptionEnum中加入
MKDIR_FAILE(10014,"文件夹创建失败"),
在productController中编写增加商品和图片上传的接口
package com.hyb.mall.controller;
import com.hyb.mall.common.ApiRestResponse;
import com.hyb.mall.common.Constant;
import com.hyb.mall.exception.MallException;
import com.hyb.mall.exception.MallExceptionEnum;
import com.hyb.mall.model.request.AddProductReq;
import com.hyb.mall.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.UUID;
/**
* 描述:后台管理Controller
*/
@RestController
public class ProductAdminController {
@Autowired
ProductService productService;
@PostMapping("admin/product/add")
public ApiRestResponse addProduct(@Valid @RequestBody AddProductReq addProductReq) {
productService.add(addProductReq);
return ApiRestResponse.success();
}
@PostMapping("admin/upload/file")
public ApiRestResponse upload(HttpServletRequest httpServletRequest,
@RequestParam("file") MultipartFile file) {
//获取原始名字
String fileName = file.getOriginalFilename();
//获取名字后缀
String suffixName = fileName.substring(fileName.lastIndexOf("."));
//生成UUID
UUID uuid = UUID.randomUUID();
String newFileName = uuid.toString() + suffixName;
//创建文件
File fileDirectory = new File(Constant.FILE_UPLOAD_DIR);
File destFile = new File(Constant.FILE_UPLOAD_DIR + newFileName);
//如果文件夹不存在则新建文件夹
if (!fileDirectory.exists()) {
if (!fileDirectory.mkdir()) {
throw new MallException(MallExceptionEnum.MKDIR_FAILE);
}
}
try {
file.transferTo(destFile);
} catch (IOException e) {
e.printStackTrace();
}
try {
return ApiRestResponse.success(getHost(new URI(httpServletRequest.getRequestURI()+""))+"/image/"+newFileName);
} catch (URISyntaxException e) {
return ApiRestResponse.error(MallExceptionEnum.UPLOAD_FAILE);
}
}
/**
* 获取IP和端口号
*/
private URI getHost(URI uri) {
URI effectiveURI;
try {
effectiveURI = new URI(uri.getScheme(), uri.getUserInfo(), uri.getHost(), uri.getPort(),
null, null, null);
} catch (URISyntaxException e) {
effectiveURI = null;
}
return effectiveURI;
}
}
在postman中进行商品添加和添加图片的操作
测试都没毛病,但是,我把图片地址信息放在浏览器上,会出现报错信息,打不开图片:原因是关于自定义静态目录资源映射目录这个点
遇到这个问题需要在config下的MallWebMvcConfig进行配置地址映射,添加上
//图片回传
registry.addResourceHandler("/images/**").
addResourceLocations("file:"+ Constant.FILE_UPLOAD_DIR);
(二)后台的更新商品接口
在request新增UpdateProduct
package com.hyb.mall.model.request;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import java.util.Date;
/**
* 描述:更新商品
*/
public class UpdateProductReq {
@NotNull(message = "商品id不能为空")
private Integer id;
private String name;
private String image;
private String detail;
private Integer categoryId;
@Min(value = 1, message = "价格不能小于1分钱")
private Integer price;
@Max(value = 10000, message = "库存不能大于1万")
private Integer stock;
private Integer status;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name == null ? null : name.trim();
}
public String getImage() {
return image;
}
public void setImage(String image) {
this.image = image == null ? null : image.trim();
}
public String getDetail() {
return detail;
}
public void setDetail(String detail) {
this.detail = detail == null ? null : detail.trim();
}
public Integer getCategoryId() {
return categoryId;
}
public void setCategoryId(Integer categoryId) {
this.categoryId = categoryId;
}
public Integer getPrice() {
return price;
}
public void setPrice(Integer price) {
this.price = price;
}
public Integer getStock() {
return stock;
}
public void setStock(Integer stock) {
this.stock = stock;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
}
在ProductServiceImpl中创建更新商品的业务逻辑
@Override
public void update(Product updateProduct){
//1.查询是否有相同的商品名
Product productOld = productMapper.selectByName(updateProduct.getName());
//2.同名且不同id不能继续修改
if (productOld != null && productOld.getId().equals(updateProduct.getId())) {
throw new MallException(MallExceptionEnum.NAME_EXISTED);
}
//3.通过校验
int count = productMapper.updateByPrimaryKeySelective(updateProduct);
if (count == 0) {
throw new MallException(MallExceptionEnum.UPDATE_FAILED);
}
}
在Productcontroller实现更新操作
@ApiOperation("后台更新商品")
@PostMapping("admin/product/update")
public ApiRestResponse update(@Valid @RequestBody UpdateCategoryReq updateCategoryReq){
Product product = new Product();
BeanUtils.copyProperties(updateCategoryReq,product);
productService.update(product);
return ApiRestResponse.success();
}
(三)后台的删除商品接口实现
在productServiceImpl根据传入的ID进行商品的删除
@Override
public void delete(Integer id) {
//1.查询商品的id主键是否存在
Product productOld = productMapper.selectByPrimaryKey(id);
if (productOld == null) {
throw new MallException(MallExceptionEnum.DELETE_FAILE);
}
//2.存在商品的id则进行删除操作
int count = productMapper.deleteByPrimaryKey(id);
if (count == 0) {
throw new MallException(MallExceptionEnum.DELETE_FAILE);
}
}
在ProductController中实现删除商品
@ApiOperation("后台删除商品")
@PostMapping("admin/product/delete")
public ApiRestResponse delete(@RequestParam Integer id){
productService.delete(id);
return ApiRestResponse.success();
}
使用postman进行接口的测试
(四)后台的批量上下架(难点)
dao层接口编写
int batchUpdateSellStatus(@Param("ids") Integer[] ids, @Param("sellStatus") Integer sellStatus);
mapper.xml SQL编写
<update id="batchUpdateSellStatus" >
update imooc_mall_product
set status = #{sellStatus}
where id in
<foreach collection="ids" close=")" item="id" open="(" separator=",">
#{id}
</foreach>
</update>
serviceImple层接口编写
@Override
public void batchUpdateSellStatus(Integer[] ids, Integer sellStatus){
productMapper.batchUpdateSellStatus(ids,sellStatus);
}
controller层接口编写
@ApiOperation("后台批量上下架")
@PostMapping("admin/product/batchUpdateSellStatus")
public ApiRestResponse batchUpdateSellStatus(@RequestParam Integer[] ids,
@RequestParam Integer sellStatus){
productService.batchUpdateSellStatus(ids,sellStatus);
return ApiRestResponse.success();
}
postman测试接口
(五)前台商品列表接口开发
dao
List<Product> selectListForAdmin();
mapper
<select id="selectListForAdmin" resultMap="BaseResultMap">
select
<include refid="Base_Column_List"/>
from imooc_mall_product
order by update_time desc
</select>
serviceImpl
@Override
public PageInfo listForAdmin(Integer pageNum, Integer pageSize){
PageHelper.startPage(pageNum,pageSize);
List<Product> products = productMapper.selectListForAdmin();
PageInfo pageInfo = new PageInfo(products);
return pageInfo;
}
controller
@ApiOperation("商品列表")
@PostMapping("admin/product/list")
public ApiRestResponse list(@RequestParam Integer pageNum,
@RequestParam Integer pageSize) {
PageInfo pageInfo = productService.listForAdmin(pageNum, pageSize);
return ApiRestResponse.success(pageInfo);
}
(六)前台商品详情接口开发
serviceImpl,直接复用之前的业务逻辑,不用重新从底层编写
@Override
public Product detail(Integer id){
Product product = productMapper.selectByPrimaryKey(id);
return product;
}
controller
@ApiOperation("商品详情")
@PostMapping("product/detail")
public ApiRestResponse detail(@RequestParam Integer id){
Product detail = productService.detail(id);
return ApiRestResponse.success(detail);
}
(七)前台商品列表接口(非常难)
具体功能实现
- 入参判空
- 加%通配符
- like关键字
前台商品列表的类
package com.hyb.mall.model.request;
public class ProductListReq {
private String keyword;
private Integer categoryId;
private String orderBy;
private Integer pageNum = 1;
private Integer pageSize = 10;
public String getKeyword() {
return keyword;
}
public void setKeyword(String keyword) {
this.keyword = keyword;
}
public Integer getCategoryId() {
return categoryId;
}
public void setCategoryId(Integer categoryId) {
this.categoryId = categoryId;
}
public String getOrderBy() {
return orderBy;
}
public void setOrderBy(String orderBy) {
this.orderBy = orderBy;
}
public Integer getPageNum() {
return pageNum;
}
public void setPageNum(Integer pageNum) {
this.pageNum = pageNum;
}
public Integer getPageSize() {
return pageSize;
}
public void setPageSize(Integer pageSize) {
this.pageSize = pageSize;
}
}
因为目录处理:如果查询某个目录下的商品,不仅需要查出来该目录,还需要查询处理子目录的所有商品。所有再新建一个quey包,新建ProductListQuery类,包含商品列表集合
package com.hyb.mall.query;
import java.util.List;
/**
* 描述:查询前台商品列表的query
*/
public class ProductListQuery {
//1.关键字
private String keyword;
//2.商品列表
private List<Integer> categoryIds;
public String getKeyWord() {
return keyword;
}
public void setKeyWord(String keyWord) {
this.keyWord = keyword;
}
public List<Integer> getCategoryIds() {
return categoryIds;
}
public void setCategoryIds(List<Integer> categoryIds) {
this.categoryIds = categoryIds;
}
}
因为用户查询的目录不是所有的目录,所以在之前编写的categoryService.listCategoryForCustomer需要进行重构:将parentId变成是用户自己传进来
@Override
@Cacheable(value = "listCategoryForCustomer") //是spring所提供的
public List<CategoryVO> listCategoryForCustomer(Integer parentId){
ArrayList<CategoryVO> categoryVOList = new ArrayList<>();
recursivelyFindCategories(categoryVOList,parentId);
return categoryVOList;
}
改动完后,还需要对service层和controller层进行同步的改动
List<CategoryVO> listCategoryForCustomer(Integer parentId);
@ApiOperation("前台商品分类列表")
@PostMapping("category/list")
@ResponseBody
public ApiRestResponse listCategoryForCustomer(){
//parentId设置成0,就能查询所有的数据
List<CategoryVO> categoryVOS = categoryService.listCategoryForCustomer(0);
return ApiRestResponse.success(categoryVOS);
}
排序处理
在constant包中创建支持排序的规则
public interface ProductListOrderBy{
Set<String> PRICE_ASC_DESC = Sets.newHashSet("price desc","price asc");
}
dao
List<Product> selectList(@Param("query") ProductListQuery query);
mapper
<select id="selectList" parameterType="com.hyb.mall.query.ProductListQuery" resultMap="BaseResultMap">
select
<include refid="Base_Column_List"/>
from imooc_mall_product
<where>
<if test="query.keyword != null">
and name like #{query.keyword}
</if>
<if test="query.categoryIds != null">
and category_id in
<foreach collection="query.categoryIds" close=")" item="item" open="(" separator=",">
#{item}
</foreach>
</if>
and status = 1
</where>
order by update_time desc
</select>
service
@Override
public PageInfo list(ProductListReq productListReq) {
//构建query对象
ProductListQuery productListQuery = new ProductListQuery();
//搜索处理
if (!StringUtils.isEmpty(productListReq.getKeyword())) {
//合成字符串,就能利用数据库的模糊查找功能
String keyword = new StringBuilder()
.append("%")
.append(productListReq.getKeyword())
.append("%")
.toString();
productListQuery.setKeyWord(keyword);
}
//3.目录处理,如果查某个目录下的商品,不仅需要查出该目录下的,还需要把所有子目录的所有商品查出来,所以要拿到一个目录id的List
if (productListReq.getCategoryId() != null) {
//这里需要对listCategoryForCustomer进行重构,
List<CategoryVO> categoryVOSList = categoryService.listCategoryForCustomer(productListReq.getCategoryId());
//得到的categoryVOSList是一个树状结构,需要进行平铺展开,将子节点的ID都拿过来
ArrayList<Integer> categoryIds = new ArrayList<>();
categoryIds.add(productListReq.getCategoryId());
getCategoryIds(categoryVOSList, categoryIds);
productListQuery.setCategoryIds(categoryIds);
}
//排序处理
//从前端请求拿到orderby
String orderBy = productListReq.getOrderBy();
if (Constant.ProductListOrderBy.PRICE_ASC_DESC.contains(orderBy)) {
PageHelper.startPage(productListReq.getPageNum(), productListReq.getPageSize(), orderBy);
} else {
PageHelper.startPage(productListReq.getPageNum(), productListReq.getPageSize());
}
List<Product> productList = productMapper.selectList(productListQuery);
PageInfo pageInfo = new PageInfo(productList);
return pageInfo;
}
private void getCategoryIds(List<CategoryVO> categoryVOList, ArrayList<Integer> categoryIds) {
for (int i = 0; i < categoryVOList.size(); i++) {
CategoryVO categoryVO = categoryVOList.get(i);
if (categoryVO != null) {
categoryIds.add(categoryVO.getId());
getCategoryIds(categoryVO.getChildCategory(), categoryIds);
}
}
}
controller
@ApiOperation("前台商品列表")
@PostMapping("product/list")
public ApiRestResponse list(@RequestParam ProductListReq productListReq){
PageInfo list = productService.list(productListReq);
return ApiRestResponse.success(list);
}
postman