实战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) {
        this.state = productStateEnum.getState();
        this.stateInfo = productStateEnum.getStateInfo();
        this.productList = productList;
    }

    /**
     * 
     * 
     * @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;

重构

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

猜你喜欢

转载自blog.csdn.net/yangshangwei/article/details/80809539