概述
步骤如下:
- 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