文章目录
- 概述
- DTO类
- 自定义异常
- ProductService接口
- 重构
- 重构后的接口方法
- 接口实现类ProductServiceImpl
- 单元测试
- Github地址
概述
步骤如下:
- 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一样。
代码语言:javascript复制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 进行了封装,代码如下
代码语言:javascript复制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 不会回滚事务。
代码语言:javascript复制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接口中的入参。
代码语言:javascript复制/**
*
*
* @Title: addShop
*
* @Description: 新增商铺
*
* @param shop
* @param shopFileInputStream
* @param fileName
* @return
*
* @return: ShopExecution
*/
ShopExecution addShop(Shop shop, InputStream shopFileInputStream, String fileName) throws ShopOperationException;
这里 商品处理,我们不仅需要处理商品的缩略图信息,还要处理商品详情中的多个图片信息,因此,定义如下
代码语言:javascript复制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方法 。
代码语言:javascript复制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中工程代码。
重构后的接口方法
代码语言:javascript复制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
代码语言:javascript复制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方法
代码语言:javascript复制/**
*
*
* @Title: generateNormalImgs
*
* @Description: 生成商品详情的图片
*
* @param prodImgDetailList
* @param relativePath
* @return
*
* @return: List
*/
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;
}
单元测试
代码语言:javascript复制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());
}
}
日志:
代码语言:javascript复制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), uploaditemshopImage52018062516132272045.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: uploaditemshopImage520180625161322338880.jpg(String), null, null, 2018-06-25 16:13:22.999(Timestamp), 6(Long), uploaditemshopImage520180625161322506811.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