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

2021-08-17 11:33:51 浏览数 (1)

文章目录

  • 概述
  • 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

0 人点赞