实战SSM_O2O商铺_29【商品】商品添加之Service层的实现及重构

简介: 实战SSM_O2O商铺_29【商品】商品添加之Service层的实现及重构

概述

步骤如下:

  • 1.处理商品的缩略图,获取相对路径,为了调用dao层的时候写入 tb_product中的 img_addr字段有值
  • 2.写入tb_product ,得到product_id(Mybatis自动映射进去的)
  • 3.集合product_id 批量处理商品详情图片
  • 4.将商品详情图片 批量更新到 tb_proudct_img表

DTO类

我们知道,我们在操作Product的时候,需要给前端返回状态信息等,单纯的domain类无法满足,这里我们使用DTO包装一下,就如同前面操作Shop和ProductCategory一样。

package com.artisan.o2o.dto;
import java.util.List;
import com.artisan.o2o.entity.Product;
import com.artisan.o2o.enums.ProductStateEnum;
/**
 * 
 * 
 * @ClassName: ProductExecution
 * 
 * @Description: 操作Product返回的DTO
 * 
 * @author: Mr.Yang
 * 
 * @date: 2018年6月25日 上午1:25:21
 */
public class ProductExecution {
  /**
   * 操作返回的状态信息
   */
  private int state;
  /**
   * 操作返回的状态信息描述
   */
  private String stateInfo;
  /**
   * 操作成功的总量
   */
  private int count;
  /**
   * 批量操作(查询商品列表)返回的Product集合
   */
  private List<Product> productList;
  /**
   * 增删改的操作返回的商品信息
   */
  private Product product;
  /**
   * 
   * 
   * @Title:ProductExecution
   * 
   * @Description:默认构造函数
   */
  public ProductExecution() {
  }
  /**
   * 
   * 
   * @Title:ProductExecution
   * 
   * @Description:批量操作成功的时候返回的ProductExecution
   * 
   * @param productStateEnum
   * @param productList
   */
  public ProductExecution(ProductStateEnum productStateEnum, List<Product> productList, int count) {
    this.state = productStateEnum.getState();
    this.stateInfo = productStateEnum.getStateInfo();
    this.productList = productList;
    this.count = count;
  }
  /**
   * 
   * 
   * @Title:ProductExecution
   * 
   * @Description:单个操作成功时返回的ProductExecution
   * 
   * @param productStateEnum
   * @param product
   */
  public ProductExecution(ProductStateEnum productStateEnum, Product product) {
    this.state = productStateEnum.getState();
    this.stateInfo = productStateEnum.getStateInfo();
    this.product = product;
  }
  /**
   * 
   * 
   * @Title:ProductExecution
   * 
   * @Description:操作失败的时候返回的ProductExecution,仅返回状态信息即可
   * 
   * @param productStateEnum
   */
  public ProductExecution(ProductStateEnum productStateEnum) {
    this.state = productStateEnum.getState();
    this.stateInfo = productStateEnum.getStateInfo();
  }
  public int getState() {
    return state;
  }
  public void setState(int state) {
    this.state = state;
  }
  public String getStateInfo() {
    return stateInfo;
  }
  public void setStateInfo(String stateInfo) {
    this.stateInfo = stateInfo;
  }
  public int getCount() {
    return count;
  }
  public void setCount(int count) {
    this.count = count;
  }
  public List<Product> getProductList() {
    return productList;
  }
  public void setProductList(List<Product> productList) {
    this.productList = productList;
  }
  public Product getProduct() {
    return product;
  }
  public void setProduct(Product product) {
    this.product = product;
  }
}

这里我们对状态和状态信息使用ProductStateEnum 进行了封装,代码如下

package com.artisan.o2o.enums;
/**
 * 
 * 
 * @ClassName: ProductStateEnum
 * 
 * @Description: 使用枚举表述常量数据字典
 * 
 * @author: Mr.Yang
 * 
 * @date: 2018年6月25日 上午1:32:23
 */
public enum ProductStateEnum {
  SUCCESS(1, "操作成功"), INNER_ERROR(-1001, "操作失败"), NULL_PARAMETER(-1002, "缺少参数");
  private int state;
  private String stateInfo;
  /**
   * 
   * 
   * @Title:ProductStateEnum
   * 
   * @Description:私有构造函数,禁止外部初始化改变定义的常量
   * 
   * @param state
   * @param stateInfo
   */
  private ProductStateEnum(int state, String stateInfo) {
    this.state = state;
    this.stateInfo = stateInfo;
  }
  /**
   * 
   * 
   * @Title: getState
   * 
   * @Description: 仅设置get方法,禁用set
   * 
   * @return
   * 
   * @return: int
   */
  public int getState() {
    return state;
  }
  public String getStateInfo() {
    return stateInfo;
  }
  /**
   * 
   * 
   * @Title: stateOf
   * 
   * @Description: 定义换成pulic static 暴漏给外部,通过state获取ShopStateEnum
   * 
   *               values()获取全部的enum常量
   * 
   * @param state
   * 
   * @return: ShopStateEnum
   */
  public static ProductStateEnum stateOf(int state) {
    for (ProductStateEnum stateEnum : values()) {
      if(stateEnum.getState() == state){
        return stateEnum;
      }
    }
    return null;
  }
}

自定义异常

操作Product 同时还要 操作商品详情的图片信息,所以必须在一个事务中,只有继承RuntimeException ,这样在标注了@Transactional事务的方法中,出现了异常,才会回滚数据。

默认情况下,如果在事务中抛出了未检查异常(继承自 RuntimeException 的异常)或者 Error,则 Spring 将回滚事务;除此之外,Spring 不会回滚事务。

package com.artisan.o2o.exception;
/**
 * 
 * 
 * @ClassName: ProductOperationException
 * 
 * @Description: 继承自RuntimeException ,这样在标注了@Transactional事务的方法中,出现了异常,才会回滚数据。
 * 
 *               默认情况下,如果在事务中抛出了未检查异常(继承自 RuntimeException 的异常)或者 Error,则 Spring
 *               将回滚事务;除此之外,Spring 不会回滚事务。
 * 
 * @author: Mr.Yang
 * 
 * @date: 2018年6月25日 下午1:46:23
 */
public class ProductOperationException extends RuntimeException {
  private static final long serialVersionUID = -6981952073033881834L;
  public ProductOperationException(String message) {
    super(message);
  }
}

ProductService接口

逻辑基本和 addShop相同,我们去看下addShop接口中的入参。

/**
   * 
   * 
   * @Title: addShop
   * 
   * @Description: 新增商铺
   * 
   * @param shop
   * @param shopFileInputStream
   * @param fileName
   * @return
   * 
   * @return: ShopExecution
   */
  ShopExecution addShop(Shop shop, InputStream shopFileInputStream, String fileName) throws ShopOperationException;

这里 商品处理,我们不仅需要处理商品的缩略图信息,还要处理商品详情中的多个图片信息,因此,定义如下

ProductExecution addProduct(Product product, InputStream prodImgIns, String prodImgName, List<InputStream> prodImgDetailInsList, List<String> prodImgDetailNameList)
      throws ProductOperationException;
• 1
• 2
• 3

重构

5个参数??? 是不是不方便Controller的调用。 这里我们大胆的重构一下,否则后面重构的话成本越来越高

我们将 InputStream prodImgIns和 String prodImgName 封装到一个类中,取名为ImageHolder ,提供构造函数用于初始化以及setter/getter方法 。

package com.artisan.o2o.dto;
import java.io.InputStream;
public class ImageHolder {
  private InputStream ins ;
  private String fileName;
  /**
   * 
   * 
   * @Title:ImageHolder
   * 
   * @Description:构造函数
   * 
   * @param ins
   * @param fileName
   */
  public ImageHolder(InputStream ins, String fileName) {
    this.ins = ins;
    this.fileName = fileName;
  }
  public InputStream getIns() {
    return ins;
  }
  public void setIns(InputStream ins) {
    this.ins = ins;
  }
  public String getFileName() {
    return fileName;
  }
  public void setFileName(String fileName) {
    this.fileName = fileName;
  }
}

之前addShop 和 modifyShop 以及 工具类中封装的方法都需要整改,涉及部分较多, 不一一列举了。

重构完成后,验证通过,详见GithuHub中工程代码。


重构后的接口方法

package com.artisan.o2o.service;
import java.io.InputStream;
import java.util.List;
import com.artisan.o2o.dto.ImageHolder;
import com.artisan.o2o.dto.ProductExecution;
import com.artisan.o2o.entity.Product;
import com.artisan.o2o.exception.ProductOperationException;
/**
 * 
 * 
 * @ClassName: ProductService
 * 
 * @Description: ProductService
 * 
 * @author: Mr.Yang
 * 
 * @date: 2018年6月25日 上午1:59:40
 */
public interface ProductService {
  /**
   * 
   * 
   * @Title: addProductDep  废弃的方法
   * 
   * @Description: 新增商品 。 因为无法从InputStream中获取文件的名称,所以需要指定文件名
   * 
   *               需要传入的参数太多,我们将InputStream 和 ImgName封装到一个实体类中,便于调用。
   * 
   *               及早进行优化整合,避免后续改造成本太大
   * 
   * @param product
   *            商品信息
   * @param prodImgIns
   *            商品缩略图输入流
   * @param prodImgName
   *            商品缩略图名称
   * @param prodImgDetailIns
   *            商品详情图片的输入流
   * @param prodImgDetailName
   *            商品详情图片的名称
   * @return
   * @throws ProductOperationException
   * 
   * @return: ProductExecution
   */
  @Deprecated
  ProductExecution addProductDep(Product product, InputStream prodImgIns, String prodImgName, List<InputStream> prodImgDetailInsList, List<String> prodImgDetailNameList)
      throws ProductOperationException;
  /**
   * 
   * 
   * @Title: addProduct
   * 
   * @Description: 重构后的addProduct
   * 
   * @param product
   *            产品信息
   * @param imageHolder
   *            产品缩略图的封装信息
   * @param prodImgDetailList
   *            产品详情图片的封装信息
   * @return
   * @throws ProductOperationException
   * 
   * @return: ProductExecution
   */
  ProductExecution addProduct(Product product, ImageHolder imageHolder, List<ImageHolder> prodImgDetailList) throws ProductOperationException;
}

接口实现类ProductServiceImpl

package com.artisan.o2o.service.impl;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.artisan.o2o.dao.ProductDao;
import com.artisan.o2o.dao.ProductImgDao;
import com.artisan.o2o.dto.ImageHolder;
import com.artisan.o2o.dto.ProductExecution;
import com.artisan.o2o.entity.Product;
import com.artisan.o2o.entity.ProductImg;
import com.artisan.o2o.enums.ProductStateEnum;
import com.artisan.o2o.exception.ProductOperationException;
import com.artisan.o2o.service.ProductService;
import com.artisan.o2o.util.FileUtil;
import com.artisan.o2o.util.ImageUtil;
/**
 * 
 * 
 * @ClassName: ProductServiceImpl
 * 
 * @Description: @Service 标识的服务层
 * 
 * @author: Mr.Yang
 * 
 * @date: 2018年6月25日 上午3:59:56
 */
@Service
public class ProductServiceImpl implements ProductService {
  @Autowired
  ProductDao productDao;
  @Autowired
  ProductImgDao productImgDao;
  @Deprecated
  @Override
  public ProductExecution addProductDep(Product product, InputStream prodImgIns, String prodImgName, List<InputStream> prodImgDetailInsList, List<String> prodImgDetailNameList)
      throws ProductOperationException {
    // 废弃的方法
    return null;
  }
  /**
   * 注意事务控制@Transactional
   * 
   * 
   * 步骤如下:
   * 
   * 1.处理商品的缩略图,获取相对路径,为了调用dao层的时候写入 tb_product中的 img_addr字段有值
   * 
   * 2.写入tb_product ,获取product_id
   * 
   * 3.集合product_id 批量处理商品详情图片
   * 
   * 4.将商品详情图片 批量更新到 tb_proudct_img表
   * 
   */
  @Override
  @Transactional
  public ProductExecution addProduct(Product product, ImageHolder imageHolder, List<ImageHolder> prodImgDetailList) throws ProductOperationException {
    if (product != null && product.getShop() != null && product.getShop().getShopId() != null && product.getProductCategory().getProductCategoryId() != null) {
      // 设置默认的属性 1 展示
      product.setCreateTime(new Date());
      product.setLastEditTime(new Date());
      product.setEnableStatus(1);
      // 如果文件的输入流和文件名不为空,添加文件到特定目录,并且将相对路径设置给product,这样product就有了ImgAddr,为下一步的插入tb_product提供了数据来源
      if (imageHolder != null) {
        addProductImg(product, imageHolder);
      }
      try {
        // 写入tb_product
        int effectNum = productDao.insertProduct(product);
        if (effectNum <= 0 ) {
          throw new ProductOperationException("商品创建失败");
        }
        // 如果添加商品成功,继续处理商品详情图片,并写入tb_product_img
        if (prodImgDetailList != null && prodImgDetailList.size() > 0) {
          addProductDetailImgs(product, prodImgDetailList);
        }
        return new ProductExecution(ProductStateEnum.SUCCESS, product);
      } catch (Exception e) {
        throw new ProductOperationException("商品创建失败:" + e.getMessage());
      }
    } else {
      return new ProductExecution(ProductStateEnum.NULL_PARAMETER);
    }
  }
  /**
   * 
   * 
   * @Title: addProductImg
   * 
   * @Description: 将商品的缩略图写到特定的shopId目录,并将imgAddr属性设置给product
   * 
   * @param product
   * @param imageHolder
   * 
   * @return: void
   */
  private void addProductImg(Product product, ImageHolder imageHolder) {
    // 根据shopId获取图片存储的相对路径
    String relativePath = FileUtil.getShopImagePath(product.getShop().getShopId());
    // 添加图片到指定的目录
    String relativeAddr = ImageUtil.generateThumbnails(imageHolder, relativePath);
    // 将relativeAddr设置给product
    product.setImgAddr(relativeAddr);
  }
  /**
   * 
   * 
   * @Title: addProductDetailImgs
   * 
   * @Description: 处理商品详情图片,并写入tb_product_img
   * 
   * @param product
   * @param prodImgDetailList
   * 
   * @return: void
   */
  private void addProductDetailImgs(Product product, List<ImageHolder> prodImgDetailList) {
    String relativePath = FileUtil.getShopImagePath(product.getShop().getShopId());
    // 生成图片详情的图片,大一些,并且不添加水印,所以另外写了一个方法,基本和generateThumbnails相似
    List<String> imgAddrList = ImageUtil.generateNormalImgs(prodImgDetailList, relativePath);
    if (imgAddrList != null && imgAddrList.size() > 0) {
      List<ProductImg> productImgList = new ArrayList<ProductImg>();
      for (String imgAddr : imgAddrList) {
        ProductImg productImg = new ProductImg();
        productImg.setImgAddr(imgAddr);
        productImg.setProductId(product.getProductId());
        productImg.setCreateTime(new Date());
        productImgList.add(productImg);
      }
      try {
        int effectedNum = productImgDao.batchInsertProductImg(productImgList);
        if (effectedNum <= 0) {
          throw new ProductOperationException("创建商品详情图片失败");
        }
      } catch (Exception e) {
        throw new ProductOperationException("创建商品详情图片失败:" + e.toString());
      }
    }
  }
}

ImageUtil#generateNormalImgs方法

/**
   * 
   * 
   * @Title: generateNormalImgs
   * 
   * @Description: 生成商品详情的图片
   * 
   * @param prodImgDetailList
   * @param relativePath
   * @return
   * 
   * @return: List<String>
   */
  public static List<String> generateNormalImgs(List<ImageHolder> prodImgDetailList, String relativePath) {
    int count = 0;
    List<String> relativeAddrList = new ArrayList<String>();
    if (prodImgDetailList != null && prodImgDetailList.size() > 0) {
      validateDestPath(relativePath);
      for (ImageHolder imgeHolder : prodImgDetailList) {
        // 1.为了防止图片的重名,不采用用户上传的文件名,系统内部采用随机命名的方式
        String randomFileName = generateRandomFileName();
        // 2.获取用户上传的文件的扩展名,用于拼接新的文件名
        String fileExtensionName = getFileExtensionName(imgeHolder.getFileName());
        // 3.拼接新的文件名 :相对路径+随机文件名+i+文件扩展名
        String relativeAddr = relativePath + randomFileName + count + fileExtensionName;
        logger.info("图片相对路径 {}", relativeAddr);
        count++;
        // 4.绝对路径的形式创建文件
        String basePath = FileUtil.getImgBasePath();
        File destFile = new File(basePath + relativeAddr);
        logger.info("图片完整路径 {}", destFile.getAbsolutePath());
        try {
          // 5. 不加水印 设置为比缩略图大一点的图片(因为是商品详情图片),生成图片
          Thumbnails.of(imgeHolder.getIns()).size(600, 300).outputQuality(0.5).toFile(destFile);
        } catch (IOException e) {
          e.printStackTrace();
          throw new RuntimeException("创建图片失败:" + e.toString());
        }
        // 将图片的相对路径名称添加到list中
        relativeAddrList.add(relativeAddr);
      }
    }
    return relativeAddrList;
  }

单元测试

package com.artisan.o2o.service;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import org.junit.Assert;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import com.artisan.o2o.BaseTest;
import com.artisan.o2o.dto.ImageHolder;
import com.artisan.o2o.dto.ProductExecution;
import com.artisan.o2o.entity.Product;
import com.artisan.o2o.entity.ProductCategory;
import com.artisan.o2o.entity.Shop;
import com.artisan.o2o.enums.ProductStateEnum;
public class ProductServiceTest extends BaseTest {
  @Autowired
  private ProductService productService;
  @Test
  public void testAddProduct() throws Exception {
    // 注意表中的外键关系,确保这些数据在对应的表中的存在
    ProductCategory productCategory = new ProductCategory();
    productCategory.setProductCategoryId(36L);
    // 注意表中的外键关系,确保这些数据在对应的表中的存在
    Shop shop = new Shop();
    shop.setShopId(5L);
    // 构造Product
    Product product = new Product();
    product.setProductName("test_product");
    product.setProductDesc("product desc");
    product.setNormalPrice("10");
    product.setPromotionPrice("8");
    product.setPriority(66);
    product.setCreateTime(new Date());
    product.setLastEditTime(new Date());
    product.setEnableStatus(1);
    product.setProductCategory(productCategory);
    product.setShop(shop);
    // 构造 商品图片
    File productFile = new File("D:/o2o/artisan.jpg");
    InputStream ins = new FileInputStream(productFile);
    ImageHolder imageHolder = new ImageHolder(ins, productFile.getName());
    // 构造商品详情图片
    List<ImageHolder> prodImgDetailList = new ArrayList<ImageHolder>();
    File productDetailFile1 = new File("D:/o2o/1.jpg");
    InputStream ins1 = new FileInputStream(productDetailFile1);
    ImageHolder imageHolder1 = new ImageHolder(ins1, productDetailFile1.getName());
    File productDetailFile2 = new File("D:/o2o/2.jpg");
    InputStream ins2 = new FileInputStream(productDetailFile2);
    ImageHolder imageHolder2 = new ImageHolder(ins2, productDetailFile2.getName());
    prodImgDetailList.add(imageHolder1);
    prodImgDetailList.add(imageHolder2);
    // 调用服务
    ProductExecution pe = productService.addProduct(product, imageHolder, prodImgDetailList);
    Assert.assertEquals(ProductStateEnum.SUCCESS.getState(), pe.getState());
  }
}

日志:

Creating a new SqlSession
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6d9f7a80]
JDBC Connection [com.mchange.v2.c3p0.impl.NewProxyConnection@61f05988] will be managed by Spring
==>  Preparing: INSERT INTO tb_product ( product_name, product_desc, img_addr, normal_price, promotion_price, priority, create_time, last_edit_time, enable_status, product_category_id, shop_id ) VALUES( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? ) 
==> Parameters: test_product(String), product desc(String), \upload\item\shopImage\5\2018062516132272045.jpg(String), 10(String), 8(String), 66(Integer), 2018-06-25 16:13:22.184(Timestamp), 2018-06-25 16:13:22.184(Timestamp), 1(Integer), 36(Long), 5(Long)
<==    Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6d9f7a80]
Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6d9f7a80] from current transaction
==>  Preparing: INSERT INTO tb_product_img ( img_addr, img_desc, priority, create_time, product_id ) VALUES ( ?, ?, ?, ?, ? ) , ( ?, ?, ?, ?, ? ) 
==> Parameters: \upload\item\shopImage\5\20180625161322338880.jpg(String), null, null, 2018-06-25 16:13:22.999(Timestamp), 6(Long), \upload\item\shopImage\5\20180625161322506811.jpg(String), null, null, 2018-06-25 16:13:22.999(Timestamp), 6(Long)
<==    Updates: 2
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6d9f7a80]

可以通过debug的方式一步步的检查参数,然后去查看数据库表中的记录和 对应的图片是正确生成。

库表数据也OK。 单元测试通过。


Github地址

代码地址: https://github.com/yangshangwei/o2o


相关文章
|
5月前
|
搜索推荐 JavaScript Java
计算机Java项目|基于SSM的个性化商铺系统
计算机Java项目|基于SSM的个性化商铺系统
|
5月前
|
JavaScript Java 测试技术
基于ssm+vue.js+uniapp小程序的商铺租赁管理系统附带文章和源代码部署视频讲解等
基于ssm+vue.js+uniapp小程序的商铺租赁管理系统附带文章和源代码部署视频讲解等
66 7
|
5月前
|
JavaScript Java 测试技术
基于ssm+vue.js+uniapp小程序的超市商品管理系统附带文章和源代码部署视频讲解等
基于ssm+vue.js+uniapp小程序的超市商品管理系统附带文章和源代码部署视频讲解等
41 4
|
6月前
|
JavaScript Java 测试技术
基于ssm+vue.js的会员制度管理的商品营销系统附带文章和源代码设计说明文档ppt
基于ssm+vue.js的会员制度管理的商品营销系统附带文章和源代码设计说明文档ppt
44 1
|
6月前
|
Java 关系型数据库 MySQL
基于SSM的商品分类管理系统
基于SSM的商品分类管理系统
67 1
|
5月前
|
JavaScript Java 测试技术
基于ssm+vue.js+uniapp小程序的二手商品网站附带文章和源代码部署视频讲解等
基于ssm+vue.js+uniapp小程序的二手商品网站附带文章和源代码部署视频讲解等
21 0
|
5月前
|
JavaScript Java 测试技术
基于ssm+vue.js+uniapp小程序的在线商品交易平台附带文章和源代码设计说明文档ppt
基于ssm+vue.js+uniapp小程序的在线商品交易平台附带文章和源代码设计说明文档ppt
76 0
|
6月前
|
SQL 测试技术
实战SSM_O2O商铺_32【商品】商品编辑之Dao层的实现
实战SSM_O2O商铺_32【商品】商品编辑之Dao层的实现
58 0
|
6月前
|
前端开发 数据库
实战SSM_O2O商铺_31【商品】商品添加之View层的实现
实战SSM_O2O商铺_31【商品】商品添加之View层的实现
46 0
|
3月前
|
Java 数据库连接 Maven
手把手教你如何搭建SSM框架、图书商城系统案例
这篇文章是关于如何搭建SSM框架以及实现一个图书商城系统的详细教程,包括了项目的配置文件整合、依赖管理、项目结构和运行效果展示,并提供了GitHub源码链接。
手把手教你如何搭建SSM框架、图书商城系统案例