springboot事务和java异常

1、记得在面试的时候曾遇到过

异常并没有被 “捕获” 到
      这是个很常见的小坑,异常并没有被 “捕获” 到,导致事务并没有回滚。我们在业务层代码中,也许已经考虑到了异常的存在,或者编辑器已经提示我们需要抛出异常,但是这里面有个需要注意的地方:并不是说我们把异常抛出来了,有异常了事务就会回滚。我们来看一个例子:

@Service
public class RoleServiceImpl implements RoleService {
 
   @Resource
   private RoleMapper roleMapper;
   
   @Override
   @Transactional
   public void saveRole(Role role) throws Exception {
       // 插入角色信息
       roleMapper.insert(role);
       // 手动抛出异常
       throw new SQLException("数据库异常");
   }
}

我们看上面这个代码,其实并没有什么问题,手动抛出一个SQLException 来模拟实际中操作数据库发生的异常,在这个方法中,既然抛出了异常,那么事务应该回滚,实际却不如此,可以自己测试一下就会发现,仍然是可以往数据库插入一条用户数据的。

那么问题出在哪呢?因为 Spring Boot 默认的事务规则是遇到运行异常(RuntimeException)和程序错误(Error)才会回滚。比如上面我们的例子中如果抛出的 RuntimeException 就没有问题,但是抛出 SQLException 就无法回滚了。

解决方法如下:

@Transactional(rollbackFor = Exception.class)
这样就没有问题了,所以在实际项目中,一定要指定异常,这是大部分开发人员不注意的地方。

异常被 “吃” 掉了
       异常怎么会被吃掉呢?还是回归到现实项目中去,我们在处理异常时,有两种方式,要么抛出去,让上一层来捕获处理;要么把异常 try…catch 掉,在异常出现的地方给处理掉。就因为有这个 try…catch,所以导致异常被 “吃” 掉,事务无法回滚。我们还是看上面那个例子,只不过简单修改一下代码:

@Service
public class RoleServiceImpl implements RoleService {
 
   @Resource
   private RoleMapper roleMapper;
 
   @Override
   @Transactional(rollbackFor = Exception.class)
   public void saveRole(Role role) {
       try {
           // 插入用户信息
           roleMapper.insert(role);
           // 手动抛出异常
           throw new SQLException("数据库异常");
       } catch (Exception e) {
           // 异常处理逻辑
       }
   }
}

      

可以自己测试一下,仍然是可以插入一条用户数据,说明事务并没有因为抛出异常而回滚。这就是 try…catch 把异 “吃” 掉了,这个细节往往比上面那个坑更难以发现,因为我们的思维方式很容易导致 try…catch 代码的产生,一旦出现这种问题,往往排查起来比较费劲。这个就是很明显的自己给自己挖坑,而且自己掉进去之后,还出不来。

那这种怎么解决呢?直接往上抛,给上一层来处理即可,千万不要在事务中把异常自己 ”吃“ 掉。

扫描二维码关注公众号,回复: 5008198 查看本文章
  • 别忘了事务是有范围的
    事务范围这个东西比上面两个坑埋的更深!我之所以把这个也写上,是因为这是我之前在实际项目中遇到的,我写一个 demo 让大家看一下,把这个坑记住即可,以后在写代码时,遇到并发问题,如果能想到这个坑,那么这篇文章也就有价值了。

@Service
public class RoleServiceImpl implements RoleService {
 
   @Resource
   private RoleMapper roleMapper;
 
   @Override
   @Transactional(rollbackFor = Exception.class)
   public synchronized void saveRole(Role role) {
       // 实际中的具体业务……
       roleMapper.insert(role);
   }
}

可以看到,因为要考虑并发问题,在业务层代码的方法上加了个 synchronized 关键字。我举个实际的场景,比如一个数据库中,针对某个角色,只有一条记录,下一个插入动作过来,会先判断该数据库中有没有相同的角色,如果有就不插入,就更新,没有才插入,所以理论上,数据库中永远就一条同一角色信息,不会出现同一数据库中插入了两条相同角色的信息。

但是在压测时,就会出现上面的问题,数据库中确实有两条同一角色的信息,那说明 synchronized 并没有起到作用。分析其原因,在于事务的范围和锁的范围问题。

从上面方法中可以看到,方法上是加了事务的,那么也就是说,在执行该方法开始时,事务启动,执行完了后,事务关闭。但是 synchronized 没有起作用,其实根本原因是因为事务的范围比锁的范围大。也就是说,在加锁的那部分代码执行完之后,锁释放掉了,但是事务还没结束,就在此时另一个线程进来了,事务没结束的话,第二个线程进来时,数据库的状态和第一个线程刚进来是一样的。即由于mysql Innodb引擎的默认隔离级别是可重复读(在同一个事务里,SELECT的结果是事务开始时时间点的状态),线程二事务开始的时候,线程一还没提交完成,导致读取的数据还没更新。第二个线程也做了插入动作,导致了脏数据。

这个问题可以避免,第一,把事务去掉即可(不推荐);第二,在调用该 service 的地方加锁,保证锁的范围比事务的范围大即可。

微服务接口

package wusc.edu.pay.facade.cost.service;

import java.util.List;
import java.util.Map;

import wusc.edu.pay.common.page.PageBean;
import wusc.edu.pay.common.page.PageParam;
import wusc.edu.pay.facade.cost.entity.CalCostInterface;
import wusc.edu.pay.facade.cost.exceptions.CostBizException;


/**
 * 
 * @描述:  成本计费接口的Dubbo服务接口.
 * @作者: WuShuicheng.
 * @创建: 2014-7-9,下午5:12:49
 * @版本: V1.0
 *
 */
public interface CalCostInterfaceFacade {
	/**
	 * 创建计费接口
	 */
	public long create(CalCostInterface entity) throws CostBizException;

	/**
	 * 修改计费接口
	 */
	public long update(CalCostInterface entity) throws CostBizException;
	
	/**
	 * 根据ID删除计费接口
	 */
	public void deleteById(long id) throws CostBizException;
	
	/**
	 * 根据计费接口编码删除计费接口
	 */
	public void deleteByCalCostInterfaceCode(String calCostInterfaceCode) throws CostBizException;
	
	/**
	 * 根据计费接口编码查找计费接口
	 */
	public CalCostInterface getByCalCostInterfaceCode(String calCostInterfaceCode) throws CostBizException;

	/**
	 * 分页查询计费接口
	 */
	public PageBean listPage(PageParam pageParam, Map<String, Object> paramMap) throws CostBizException;

	/**
	 * 根据ID查找计费接口
	 */
	public CalCostInterface getById(long id) throws CostBizException;
	
	/**
	 * 获取所有计费接口
	 */
	public List<CalCostInterface> listAll() throws CostBizException;
}

微服务实现Dao层

package wusc.edu.pay.core.cost.dao;

import wusc.edu.pay.common.core.dao.BaseDao;
import wusc.edu.pay.facade.cost.entity.CalCostInterface;


/**
 * 
 * @描述: 成本计费接口表的数据访问层接口.
 * @作者: WuShuicheng.
 * @创建: 2014-7-9,下午5:17:04
 * @版本: V1.0
 *
 */
public interface CalCostInterfaceDao extends BaseDao<CalCostInterface>{

	/**
	 * 根据计费接口编码获取计费接口信息
	 * @param interfaceCode 计费接口编码
	 * @return
	 */
	public CalCostInterface getByInterfaceCode(String interfaceCode);

	/**
	 * 根据计费接口编码删除计费接口信息
	 * @param calCostInterfaceCode
	 */
	public void deleteByCalCostInterfaceCode(String calCostInterfaceCode);

}

微服务实现service层

package wusc.edu.pay.core.cost.biz;

import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import wusc.edu.pay.common.core.biz.BaseBizImpl;
import wusc.edu.pay.common.core.dao.BaseDao;
import wusc.edu.pay.common.enums.PublicStatusEnum;
import wusc.edu.pay.common.utils.CheckUtils;
import wusc.edu.pay.core.cost.biz.cal.BankCostFactory;
import wusc.edu.pay.core.cost.biz.cal.abs.AbstractBankCost;
import wusc.edu.pay.core.cost.dao.CalCostOrderDao;
import wusc.edu.pay.facade.cost.entity.CalCostInterface;
import wusc.edu.pay.facade.cost.entity.CalCostOrder;
import wusc.edu.pay.facade.cost.entity.CalDimension;
import wusc.edu.pay.facade.cost.entity.CalFeeFlow;
import wusc.edu.pay.facade.cost.entity.CalFeeRateFormula;
import wusc.edu.pay.facade.cost.entity.CalFeeWay;
import wusc.edu.pay.facade.cost.enums.CalApproximationEnum;
import wusc.edu.pay.facade.cost.enums.CalTypeEnum;
import wusc.edu.pay.facade.cost.enums.CostInterfacePolicyEnum;
import wusc.edu.pay.facade.cost.enums.CostItemEnum;
import wusc.edu.pay.facade.cost.enums.SystemResourceTypeEnum;
import wusc.edu.pay.facade.cost.exceptions.CostBizException;


/**
 * 
 * @描述: 成本订单信息表业务实现类 .
 * @作者: huqian .
 * @创建时间: 2014-7-1, 上午11:35:52
 */
@Component("calCostOrderBiz")
public class CalCostOrderBiz extends BaseBizImpl<CalCostOrder> {

	@Autowired
	private CalCostOrderDao calCostOrderDao;

	@Autowired
	private CalDimensionBiz calDimensionBiz;
	@Autowired
	private CalFeeFlowBiz calFeeFlowBiz;
	@Autowired
	private CalFeeWayBiz calFeeWayBiz;
	@Autowired
	private CalFeeRateFormulaBiz calFeeRateFormulaBiz;
	@Autowired
	private CalCostInterfaceBiz calCostInterfaceBiz;

	/**
	 * log4j日志记录
	 */
	private Log logger = LogFactory.getLog(CalCostOrderBiz.class);

	protected BaseDao<CalCostOrder> getDao() {
		return calCostOrderDao;
	}

	/**
	 * 根据银行订单号查询
	 * 
	 * @param bankOrderNo
	 * @return
	 */
	public CalCostOrder getBybankOrderNo(String bankOrderNo) {
		return calCostOrderDao.getBybankOrderNo(bankOrderNo);
	}

	/**
	 * 保存订单和流量信息
	 * 
	 * @param order
	 *            订单信息
	 * @param feeFlow
	 *            流量信息
	 * @throws CostBizException
	 */
	@Transactional(rollbackFor = Exception.class, readOnly = false)
	public void create(CalCostOrder order, CalFeeFlow feeFlow) throws CostBizException {
		/** 判断交易流水号是否重复 **/
		if (this.isDoubleOrder(order)) {
			throw CostBizException.CAL_FEE_ERROR.newInstance("交易流水号[%s]已经存在", order.getTrxNo());
		}
		try {
			/** 验证客户端上送的信息是否完整 **/
			logger.info("验证消息队列上送的成本订单信息");
			if (this.createInfoValidate(order)) {
				logger.info(String.format("成本订单验证通过,银行接口[%s],交易金额[%s],交易流水[%s],成本计费项[%s]", order.getCalInterface(), order.getAmount(),
						order.getTrxNo(), order.getCostItem()));
			} else {
				throw CostBizException.COST_ORDER_INVALID;
			}
		} catch (Exception e) {
			order.setStatus(PublicStatusEnum.INACTIVE.getValue());
			order.setFailedReason(e.getMessage());
			logger.error("银行成本保存失败", e);
			throw CostBizException.CAL_FEE_ERROR.newInstance("银行成本保存失败, %s", e.getMessage());
		} finally {
			/** 保存银行成本信息 **/
			order.setCalEndTime(new Date());
			/** 保存订单信息和流量信息 **/
			logger.info("保存成本订单信息");
			this.create(order);
			if (feeFlow != null) {
				if (feeFlow.getId() != null && feeFlow.getId() > 0) {
					calFeeFlowBiz.update(feeFlow);
				} else {
					calFeeFlowBiz.create(feeFlow);
				}
			}
			logger.info("银行成本保存完成");
		}
	}

	/**
	 * <pre>
	 * 	根据系统来源和交易流水号获取成本订单信息
	 * 	* 由于(系统来源 + 交易流水号)在数据库的成本订单表中是唯一的,故只能获取到一个值
	 * </pre>
	 * 
	 * @param fromSystem
	 *            系统来源
	 * @param trxno
	 *            交易流水号
	 * @return
	 */
	public CalCostOrder getByTrxno(String fromSystem, String trxno) {
		return calCostOrderDao.getByTrxno(fromSystem, trxno);
	}

	/**
	 * <pre>
	 * 	验证客户端上送的成本订单信息
	 * 	验证的信息有:
	 * 		1、银行接口		-	用于获取计费维度、计费约束
	 * 		2、交易金额		-	用于从计费公式中获取有效的规则
	 * 		3、交易流水号	-	用于判定重复交易和获取原交易
	 * 		4、交易类型		-	用于判定是否存在原交易
	 * </pre>
	 * 
	 * @param order
	 *            成本订单信息
	 * @return
	 */
	private boolean createInfoValidate(CalCostOrder order) {
		if (order == null) {
			logger.error("客户端上送的订单信息不能为空");
			return false;
		}
		if (CheckUtils.isEmpty(order.getCalInterface())) {
			logger.error("银行接口信息不能为空");
			return false;
		}
		if (CheckUtils.isEmpty(order.getAmount())) {
			logger.error("交易金额不能为空");
			return false;
		}
		if (CheckUtils.isEmpty(order.getFromSystem())) {
			logger.error("系统来源不能为空");
			return false;
		}
		if (CheckUtils.isEmpty(order.getTrxNo())) {
			logger.error("交易流水号不能为空");
			return false;
		}
		if (order.getAmount().compareTo(BigDecimal.valueOf(0)) <= 0) {
			logger.error(String.format("交易金额[%s]有误", order.getAmount()));
			return false;
		}
		if (CheckUtils.isEmpty(order.getCostItem()) || CostItemEnum.getEnum(order.getCostItem()) == null) {
			logger.error(String.format("成本计费项[%s]有误", order.getCostItem()));
			return false;
		}
		return true;
	}

	/**
	 * <pre>
	 * 	根据交易类型判断该交易是否存在原交易
	 * 	需要计算手续费的成本订单存在原交易的判定依据:
	 * 		1、所有的退款(或退货)交易
	 * 		2、所有的撤销交易
	 * 		3、所有的冲正交易
	 * </pre>
	 * 
	 * @param order
	 *            客户端上送的成本订单信息
	 * @return
	 */
	private boolean existOriginalInfo(CalCostOrder order) {
		return !CheckUtils.isEmpty(order.getOrgTrxNo());
	}

	/**
	 * <pre>
	 * 	判断是否属于重复交易
	 * 	注意事项
	 * 		* 由于在成本订单表中,交易流水号是唯一的,故根据交易流水号来判定。
	 * 		* 在此设置成本订单的创建时间
	 * </pre>
	 * 
	 * @param order
	 *            客户端上送的成本订单信息
	 * @return
	 */
	private boolean isDoubleOrder(CalCostOrder order) {
		if (order == null || CheckUtils.isEmpty(order.getTrxNo())) {
			return false;
		}
		order.setCreateTime(new Date());
		return this.getByTrxno(order.getFromSystem(), order.getTrxNo()) != null;
	}

	/**
	 * 根据原交易流水号获取手续费
	 * 
	 * @param amount
	 *            本次退款(货)交易的退款金额
	 * @param fromSystem
	 *            系统来源
	 * @param orgTrxNo
	 *            原交易流水号
	 * @param orgOrder
	 *            原交易订单信息
	 * @return
	 */
	private BigDecimal getOrgOrderFee(BigDecimal amount, CalCostOrder orgOrder) throws CostBizException {
		logger.info("判断是否属于全部退款(货)");
		if (amount.compareTo(orgOrder.getAmount()) == 0) {
			// 全部退款(货)
			logger.info(String.format("该交易属于全部退款(货),手续费[%s]将全额退", orgOrder.getFee()));
			return orgOrder.getFee();
		}
		logger.info(String.format("根据系统来源[%s]和原交易流水号[%s]获取已退款(货)的交易信息", orgOrder.getFromSystem(), orgOrder.getTrxNo()));
		BigDecimal hadRefundAmount = BigDecimal.ZERO;
		BigDecimal hadRefundFee = BigDecimal.ZERO;
		List<CalCostOrder> orgOrders = calCostOrderDao.listByOrgTrxNo(orgOrder.getFromSystem(), orgOrder.getTrxNo());
		int size = (orgOrders == null) ? 0 : orgOrders.size();
		logger.info(String.format("找到了[%d]笔已退款的信息", size));
		if (size > 0) {
			for (int i = 0; i < size; i++) {
				CalCostOrder order = orgOrders.get(i);
				if (order == null)
					continue;
				logger.info(String.format("已退款订单[%d/%d],交易金额[%s],手续费[%s],交易状态[%s]", i + 1, size, order.getAmount(), order.getFee(),
						order.getStatus()));
				if (order.getAmount() == null)
					continue;
				if (order.getFee() == null)
					continue;
				if (order.getStatus().intValue() != PublicStatusEnum.ACTIVE.getValue()) {
					continue;
				}
				hadRefundAmount = hadRefundAmount.add(order.getAmount());
				hadRefundFee = hadRefundFee.add(order.getFee());
			}
		}
		logger.info(String.format("已退款(货)的交易金额为[%s], 手续费为[%s]", hadRefundAmount, hadRefundFee));
		int flag = hadRefundAmount.add(amount).compareTo(orgOrder.getAmount());
		if (flag == 0) {
			// 最后一次退款
			BigDecimal leftFee = orgOrder.getFee().subtract(hadRefundFee);
			logger.info(String.format("该交易属于最后一次退款,则将剩余的手续费[%s]全部退还", leftFee));
			return leftFee.setScale(2, BigDecimal.ROUND_HALF_UP);
		} else if (flag == 1) {
			throw CostBizException.CAL_FEE_ERROR.newInstance("退款的总金额[%s]超过了原交易金额[%s]", hadRefundAmount.add(amount), orgOrder.getAmount());
		} else {
			BigDecimal fee = orgOrder.getFee().multiply(amount).divide(orgOrder.getAmount(), 6, BigDecimal.ROUND_HALF_UP);
			logger.info(String.format("该交易将按原交易的手续费[%s]比例退还手续费[%s]", orgOrder.getFee(), fee));
			return fee.setScale(2, BigDecimal.ROUND_HALF_UP);
		}
	}

	/**
	 * <pre>
	 * 	处理带有原交易的订单信息
	 * 	前提:
	 * 		成本订单必须带有原交易
	 * 	流程:
	 * 		1、验证客户端有没有上送原交易流水号
	 * 		2、根据原交易流水号查找原交易订单信息
	 * 			2.1 如果找不到则抛出异常
	 * 			2.2 如果找到了则继续下一步
	 * 		3、将原交易的信息设置到当前交易中
	 * 			需要设置的属性有:手续费、计费表达式、订单类型、约束编号、订单状态;
	 * 			创建时间和计费截止时间取当前时间。
	 * 		4、如果原交易没有成功,则抛出异常
	 * </pre>
	 * 
	 * @param order
	 *            客户端上送的成本订单信息
	 * @param date
	 *            当前的时间
	 * @throws CostBizException
	 */
	private void processOrigOrder(CalCostOrder order, Date date) throws CostBizException {
		logger.info("该订单需要验证原交易信息");
		CalCostOrder origOrder = this.getByTrxno(order.getFromSystem(), order.getOrgTrxNo());
		if (origOrder == null) {
			throw CostBizException.CAL_FEE_ERROR.newInstance("找不到原交易流水号[%s]", order.getOrgTrxNo());
		} else {
			logger.info(String.format("原交易: 成本计费项=[%s],原交易时间=[%s],原交易流水号=[%s],原交易金额=[%s],原交易手续费=[%s]", origOrder.getCostItem(),
					origOrder.getTrxTime(), origOrder.getTrxNo(), origOrder.getAmount(), origOrder.getFee()));
			order.setFee(this.getOrgOrderFee(order.getAmount(), origOrder));
			order.setCalExpression(origOrder.getCalExpression());
			order.setCalOrderType(origOrder.getCalOrderType());
			order.setFeeWayId(origOrder.getFeeWayId());
			order.setStatus(origOrder.getStatus());
			order.setCreateTime(date);
			order.setCalEndTime(date);
			if (order.getStatus().intValue() == PublicStatusEnum.INACTIVE.getValue()) {
				order.setFailedReason("原交易没有计费成功");
				throw CostBizException.CAL_FEE_ERROR.newInstance("原交易没有计费成功[%s]", origOrder.getFailedReason());
			} else {
				CalFeeWay calFeeWay = calFeeWayBiz.getById(origOrder.getFeeWayId());
				if (calFeeWay == null) {
					order.setFailedReason("找不到原交易的计费约束");
					throw CostBizException.CAL_FEE_ERROR.newInstance("找不到原交易的计费约束[%s]", origOrder.getFeeWayId());
				}
				int calType = calFeeWay.getCalType().intValue();
				if (calType == CalTypeEnum.LADDER_SINGLE.getValue() || calType == CalTypeEnum.LADDER_MULTIPLE.getValue()) {
					logger.info("原交易属于阶梯类型的订单,故需要在流量表中做相应调整");
					if (calFeeWay.getCycleType() == null) {
						throw CostBizException.CAL_CYCLE_DATE_ERROR.newInstance("计费周期类型未设置");
					}
					SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
					Date beginDate = calFeeFlowBiz.fetchCycleBeginDate(calFeeWay, origOrder.getCreateTime());
					Date endDate = calFeeFlowBiz.fetchCycleEndDate(calFeeWay, origOrder.getCreateTime());
					if (endDate.before(date)) {
						throw CostBizException.CAL_FEE_ERROR.newInstance("原交易的计费周期[%s => %s]已过期", sdf.format(beginDate),
								sdf.format(endDate));
					}
					logger.info(String.format("根据原交易的计费周期获取原交易的计费流量", sdf.format(beginDate), sdf.format(endDate)));
					CalFeeFlow calFeeFlow = calFeeFlowBiz.fetchCalFeeFlow(calFeeWay, beginDate, endDate);
					if (calFeeFlow == null) {
						throw CostBizException.CAL_FEE_ERROR.newInstance("找不到原交易的计费周期[%s => %s]", sdf.format(beginDate),
								sdf.format(endDate));
					}
					calFeeFlow.setThisAmount(order.getAmount());
					logger.info(String.format("计费流量更新前的总金额:%s", calFeeFlow.getTotalAmount()));

					// TODO 佳龙 成本订单增加->正向交易还是退款,类似商户计费
					calFeeFlow.setTotalAmount(calFeeFlow.getTotalAmount().add(order.getAmount()));

					calFeeFlow.setModifyTime(date);
					logger.info(String.format("计费流量更新后的总金额:%s", calFeeFlow.getTotalAmount()));
					order.setFeeFlow(calFeeFlow);
				}
			}
		}
	}

	/**
	 * <pre>
	 * 	根据订单信息实时计算成本信息
	 * 	计算流程:
	 * 		1、判断交易流水号是否重复
	 * 			1.1  如果重复,则直接抛出异常给客户端(该类型的交易不需要入库)
	 * 			1.2 如果不重复,则继续下一步
	 * 		2、验证客户端上送的信息是否完整
	 * 			2.1 如果不完整,则抛出异常,然后保存银行成本。
	 * 			2.2 如果完整,则继续下一步
	 * 		3、判断该交易是否存在原交易
	 * 			3.1 如果存在原交易,则获取原交易信息并进行处理,然后保存银行成本。
	 * 			3.2 如果不存在原交易,则继续下一步
	 * 		4、计算银行手续费,并且设置到当前订单信息中
	 * 			4.1 如果计算失败,则抛出异常(提示找不到计费规则),然后保存银行成本。
	 * 			4.2 如果计算成功,则继续下一步
	 * 		5、保存银行成本信息
	 * 		6、返回手续费给客户端
	 * </pre>
	 * 
	 * @param order
	 *            订单信息
	 * @return
	 * @throws CostBizException
	 */
	public CalCostOrder calulateBankCost(CalCostOrder order, CostItemEnum costItemEnum, SystemResourceTypeEnum systemResourceTypeEnum) {
		try {
			logger.info(String.format("接收到计费请求:\r\n\t计费接口=[%s],计费金额=[%s],计费项=[%s],原交易流水=[%s],系统来源=[%s]", order.getCalInterface(),
					order.getAmount(), costItemEnum, order.getOrgTrxNo(), systemResourceTypeEnum));
			if (CheckUtils.isEmpty(order.getCalInterface())) {
				throw CostBizException.CAL_INTERFACE_NOEXIST;
			}
			if (order.getAmount() == null || order.getAmount().compareTo(BigDecimal.valueOf(0)) < 1) {
				throw CostBizException.CAL_FEE_ERROR.newInstance("交易金额[%s]有误", order.getAmount());
			}
			if (costItemEnum == null) {
				throw CostBizException.CAL_FEE_ERROR.newInstance("计费项[%s]有误", order.getCostItem());
			} else {
				order.setCostItem(costItemEnum.getValue());
			}
			if (order.getCostItem() == null || order.getCostItem().intValue() <= 0) {
				throw CostBizException.CAL_FEE_ERROR.newInstance("找不到计费项[%s]", order.getCostItem());
			}
			if (systemResourceTypeEnum != null) {
				order.setFromSystem(Integer.toString(systemResourceTypeEnum.getValue()));
			}
			String mccTypeCode = null;
			if (order.getMcc() != null && !order.getMcc().trim().equals("")) {
				// 在线交易,直接把mcc赋给mccTypeCode
				mccTypeCode = order.getMcc();
			}
			Date date = new Date();
			/** 判断该订单是否属于包年订单 **/
			CalCostInterface calCostInterface = calCostInterfaceBiz.getByInterfaceCode(order.getCalInterface());
			if (calCostInterface != null && calCostInterface.getPolicy().intValue() == CostInterfacePolicyEnum.YEAR.getValue()) {
				/** 处理包年订单 **/
				order.setCalExpression("包年:0");
				order.setFee(BigDecimal.ZERO);
				order.setStatus(PublicStatusEnum.ACTIVE.getValue());
			} else {
				/** 判断该交易是否存在原交易 **/
				if (this.existOriginalInfo(order)) {
					/** 处理原交易订单 **/
					this.processOrigOrder(order, date);
				} else {
					/** 计算银行手续费,并且设置到当前订单信息中 **/
					this.calculateBankFee(order, date, mccTypeCode);
					order.setStatus(PublicStatusEnum.ACTIVE.getValue());
				}
			}
			return order;
		} catch (Exception e) {
			order.setStatus(PublicStatusEnum.INACTIVE.getValue());
			order.setFailedReason(e.getMessage());
			logger.error("银行成本预算失败", e);
			throw CostBizException.CAL_FEE_ERROR.newInstance("银行成本预算失败, %s", e.getMessage());
		} finally {
			logger.info("银行成本预算完成");
		}
	}

	/**
	 * <pre>
	 * 	计算银行手续费,并且设置到当前订单信息中
	 * 	计算流程:
	 * 		1、根据计费接口查找计费维度
	 * 			1.1 如果找不到计费维度,则抛出异常
	 * 			1.2 如果找到了计费维度,则继续下一步
	 * 		2、轮循查找到的计费维度,根据计费维度编号查询计费约束
	 * 			2.1 如果找不到计费约束,则继续循环
	 * 			2.2如果找到了计费约束,则继续下一步
	 * 		3、轮循查找到的计费约束,验证计费约束是否有效
	 * 			3.1 如果无效,则继续循环
	 * 			3.2 如果有效,则继续下一步
	 * 		4、根据计费约束查找计费公式
	 * 			4.1 如果计费公式适合当前的交易金额(或者交易总金额),则根据此金额计算手续费并返回结果
	 * 			4.2 如果计费公式不适合当前的交易金额(或者交易总金额),则继续循环
	 * 		5、如果一直找不到有效的计费公式,则抛出异常
	 * </pre>
	 * 
	 * @param order
	 *            客户端上送的订单信息
	 * @param date
	 *            当前的时间
	 * @return
	 */
	private void calculateBankFee(CalCostOrder order, Date date, String mccTypeCode) throws CostBizException {
		/** 根据计费接口查找计费维度 **/
		logger.info(String.format("根据计费接口[%s]查询计费维度", order.getCalInterface()));
		List<CalDimension> dims = calDimensionBiz.listByBankInterface(order.getCalInterface());
		int dimCount = (dims == null) ? 0 : dims.size();
		if (dimCount == 0) {
			throw CostBizException.CAL_FEE_ERROR.newInstance("找不到计费接口[%s]对应的计费维度", order.getCalInterface());
		} else {
			logger.info(String.format("计费维度查询成功,找到[%d]个计费维度", dimCount));
		}
		for (int i = 0; i < dimCount; i++) {
			CalDimension dim = dims.get(i);
			/** 根据计费维度查找计费约束 **/
			logger.info(String.format("根据计费维度[产品名称: %s; 银行接口: %s]查询计费约束[%d/%d]", dim.getCalProduct(), dim.getCalCostInterfaceCode(), i + 1,
					dimCount));
			List<CalFeeWay> constraints = calFeeWayBiz.listByDimensionId(dim.getId());
			int constraintCount = (constraints == null) ? 0 : constraints.size();
			logger.info(String.format("查询成功,查询到[%d]条计费约束", constraintCount));
			for (int j = 0; j < constraintCount; j++) {
				CalFeeWay constraint = constraints.get(j);
				/** 根据计费约束查找计费公式 **/
				logger.info(String.format("[%d/%d]验证计费约束[约束名称: %s]", j + 1, constraintCount, constraint.getWayName()));
				if (calFeeWayBiz.validate(constraint, mccTypeCode)) {
					logger.info(String.format("[%d/%d]计费约束[约束名称: %s]验证成功", j + 1, constraintCount, constraint.getWayName()));
				} else {
					logger.info(String.format("[%d/%d]计费约束[约束名称: %s]验证拒绝", j + 1, constraintCount, constraint.getWayName()));
					continue;
				}
				logger.info(String.format("[%d/%d]根据计费约束[约束名称: %s]查找计费公式", j + 1, constraintCount, constraint.getWayName()));
				AbstractBankCost bankCost = BankCostFactory.newInstance(order, calFeeFlowBiz, constraint, date);
				List<CalFeeRateFormula> formulas = bankCost.getFormula(calFeeRateFormulaBiz, constraint);
				if (bankCost.calculate(formulas)) {
					BigDecimal fee = BigDecimal.ZERO;
					
//					if (constraint.getIsRound() != null && constraint.getIsRound() == PublicStatusEnum.ACTIVE.getValue()) { // 鍒ゆ柇鏄惁瑕佽繘琛屽洓鑸嶄簲鍏�
//						fee = bankCost.getFee().setScale(2, BigDecimal.ROUND_HALF_UP);
//					} else {
//						fee = bankCost.getFee();
//					}
					
					if (constraint.getIsRound() == CalApproximationEnum.NONE.getValue()) { // NONE 不做任何操作
						fee = bankCost.getFee();
					} else if (constraint.getIsRound() == CalApproximationEnum.LAST_ROUND.getValue()) {// 舍尾法
						fee = bankCost.getFee().setScale(2, BigDecimal.ROUND_DOWN);
					} else if (constraint.getIsRound() == CalApproximationEnum.INTO_LAW.getValue()) {// 进一法
						fee = bankCost.getFee().setScale(2, BigDecimal.ROUND_UP);
					} else {// 四舍五入法
						fee = bankCost.getFee().setScale(2, BigDecimal.ROUND_HALF_UP);
					}
					
					order.setFee(fee);
					order.setFeeWayId(constraint.getId());
					bankCost.saveFlowInfo();
					logger.info(String.format("银行成本计算成功,手续费[%s]", order.getFee()));
					return;
				} else {
					logger.warn(String.format("计费约束[约束名称: %s]找不到有效的计费公式[%d/%d]", constraint.getWayName(), j + 1, constraintCount));
					continue;
				}
			}
		}
		throw CostBizException.CAL_FEE_ERROR.newInstance("找不到有效的计费规则");
	}
	

	/**
	 * 根据计费项和支付流水号查询成本订单
	 * 
	 * @param trxNo
	 * @param costItem
	 * @return
	 */
	public CalCostOrder getByPayTrxNoAndCostItem(String trxNo, Integer costItem) {
		Map<String, Object> paramMap = new HashMap<String, Object>();
		paramMap.put("trxNo", trxNo);
		paramMap.put("costItem", costItem);
		return calCostOrderDao.getBy(paramMap);
	}

}

微服务接口实现

package wusc.edu.pay.facade.cost.service.impl;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import wusc.edu.pay.common.page.PageBean;
import wusc.edu.pay.common.page.PageParam;
import wusc.edu.pay.core.cost.biz.CalCostInterfaceBiz;
import wusc.edu.pay.core.cost.dao.CalCostInterfaceDao;
import wusc.edu.pay.facade.cost.entity.CalCostInterface;
import wusc.edu.pay.facade.cost.exceptions.CostBizException;
import wusc.edu.pay.facade.cost.service.CalCostInterfaceFacade;


/**
 * 
 * @描述: 成本计费接口的Dubbo服务接口的实现类.
 * @作者: WuShuicheng.
 * @创建: 2014-7-9,下午5:14:23
 * @版本: V1.0
 *
 */
@Component("calCostInterfaceFacade")
public class CalCostInterfaceFacadeImpl implements CalCostInterfaceFacade {
	@Autowired
	private CalCostInterfaceBiz calCostInterfaceBiz;
	@Autowired
	private CalCostInterfaceDao calCostInterfaceDao;

	@Override
	public long create(CalCostInterface entity) throws CostBizException {
//		return calCostInterfaceDao.insert(entity);
		return calCostInterfaceBiz.createCostInterface(entity);
	}

	@Override
	public long update(CalCostInterface entity) throws CostBizException {
		return calCostInterfaceBiz.updateCostInterface(entity);
	}

	@Override
	public PageBean listPage(PageParam pageParam, Map<String, Object> paramMap) throws CostBizException {
		return calCostInterfaceDao.listPage(pageParam, paramMap);
	}

	@Override
	public CalCostInterface getById(long id) throws CostBizException {
		return calCostInterfaceDao.getById(id);
	}

	@Override
	public void deleteById(long id) throws CostBizException {
		calCostInterfaceDao.deleteById(id);
		
	}

	@Override
	public void deleteByCalCostInterfaceCode(String calCostInterfaceCode) throws CostBizException {
		calCostInterfaceDao.deleteByCalCostInterfaceCode(calCostInterfaceCode);
	}

	@Override
	public CalCostInterface getByCalCostInterfaceCode(String calCostInterfaceCode) throws CostBizException {
		return calCostInterfaceBiz.getByCalCostInterfaceCode(calCostInterfaceCode);
	}

	@Override
	public List<CalCostInterface> listAll() throws CostBizException {
		return calCostInterfaceDao.listBy(new HashMap<String,Object>());
	}
	

}

异常

package wusc.edu.pay.common.exceptions;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * 业务异常基类,所有业务异常都必须继承于此异常
 * 
 * @author healy
 * 
 *         定义异常时,需要先确定异常所属模块。例如:添加商户报错 可以定义为 [10020001] 前四位数为系统模块编号,后4位为错误代码 ,唯一 <br>
 *         商户门户异常 1002 <br>
 *         会员门户异常 1004 <br>
 *         boss门户异常 1005 <br>
 *         商户API 异常 1008 <br>
 *         支付网关异常 1009 <br>
 *         会计门户异常 1010 <br>
 *         通知应用异常 1011 <br>
 *         银行服务异常 1012 <br>
 *         银行后置异常 1013 <br>
 *         支付规则异常 1015 <br>
 *         用户服务异常 2002 <br>
 *         boss服务异常 2005 <br>
 *         结算服务异常 2006 <br>
 *         订单服务异常 2007 <br>
 *         账户服务异常 2008 <br>
 *         退款服务异常 2009 <br>
 *         会计服务异常 2010 <br>
 *         通知服务异常 2011 <br>
 *         商户接口异常2012 <br>
 *         证书异常 3001 <br>
 *         风控异常 4001 <br>
 *         计费异常 5001 <br>
 *         成本计费异常 6001 <br>
 *         限制开关异常 7001 <br>
 *         限制开关(业务)异常 7002 <br>
 *         限制开关(金额限制)异常 7003 <br>
 *         银行打款异常 8001 <br>
 */
public class BizException extends RuntimeException {

	private static final long serialVersionUID = -5875371379845226068L;

	/**
	 * 数据库操作,insert返回0
	 */
	public static final BizException DB_INSERT_RESULT_0 = new BizException(90040001, "数据库操作,insert返回0");

	/**
	 * 数据库操作,update返回0
	 */
	public static final BizException DB_UPDATE_RESULT_0 = new BizException(90040002, "数据库操作,update返回0");

	/**
	 * 数据库操作,selectOne返回null
	 */
	public static final BizException DB_SELECTONE_IS_NULL = new BizException(90040003, "数据库操作,selectOne返回null");

	/**
	 * 数据库操作,list返回null
	 */
	public static final BizException DB_LIST_IS_NULL = new BizException(90040004, "数据库操作,list返回null");

	/**
	 * Token 验证不通过
	 */
	public static final BizException TOKEN_IS_ILLICIT = new BizException(90040005, "Token 验证非法");
	/**
	 * 会话超时 获取session时,如果是空,throws 下面这个异常 拦截器会拦截爆会话超时页面
	 */
	public static final BizException SESSION_IS_OUT_TIME = new BizException(90040006, "会话超时");

	/**
	 * 获取序列出错
	 */
	public static final BizException DB_GET_SEQ_NEXT_VALUE_ERROR = new BizException(90040007, "获取序列出错");

	/**
	 * 异常信息
	 */
	protected String msg;

	/**
	 * 具体异常码
	 */
	protected int code;

	public BizException(int code, String msgFormat, Object... args) {
		super(String.format(msgFormat, args));
		this.code = code;
		this.msg = String.format(msgFormat, args);
	}

	public BizException() {
		super();
	}

	public String getMsg() {
		return msg;
	}

	public int getCode() {
		return code;
	}

	/**
	 * 实例化异常
	 * 
	 * @param msgFormat
	 * @param args
	 * @return
	 */
	public BizException newInstance(String msgFormat, Object... args) {
		return new BizException(this.code, msgFormat, args);
	}

	public BizException(String message, Throwable cause) {
		super(message, cause);
	}

	public BizException(Throwable cause) {
		super(cause);
	}

	public BizException(String message) {
		super(message);
	}
}
package wusc.edu.pay.facade.cost.exceptions;

import wusc.edu.pay.common.exceptions.BizException;

public class CostBizException extends BizException{

	private static final long serialVersionUID = -2814707659216158933L;
	
	public static final CostBizException DIMENSION_IS_EXIST = new CostBizException(60010001,"维度已存在");

	public static final CostBizException DIMENSION_NOEXIST = new CostBizException(60010002,"找不到计费维度信息");
	

	public static final CostBizException COST_ORDER_INVALID = new CostBizException(60010003,"成本订单信息验证失败");
	

	public static final CostBizException CAL_INTERFACE_NOEXIST = new CostBizException(60010004,"找不到银行计费接口");
	
	public static final CostBizException CAL_RULE_NO_FOUND = new CostBizException(60010005,"找不到有效的计费规则");

	public static final CostBizException CAL_CYCLE_DATE_ERROR = new CostBizException(60010006,"计费周期设置有误");
	
	public static final CostBizException CAL_FLOW_SAVE_ERROR = new CostBizException(60010007,"计费流量保存出现异常");

	public static final CostBizException CAL_FEE_ERROR = new CostBizException(60010008,"计算费率成本出现异常");
	
	public static final CostBizException COST_INTERFACE_IS_EXIST = new CostBizException(60010009,"计费接口已经存在");
	
	public static final CostBizException COST_ORDER_NOT_EXIST = new CostBizException(60010010,"成本订单不存在");
	
	
	public CostBizException() {
	}

	public CostBizException(int code, String msgFormat, Object... args) {
		super(code, msgFormat, args);
	}

	public CostBizException(int code, String msg) {
		super(code, msg);
	}
	
	/**
	 * 实例化异常
	 * 
	 * @param msgFormat
	 * @param args
	 * @return
	 */
	public CostBizException newInstance(String msgFormat, Object... args) {
		return new CostBizException(this.code, msgFormat, args);
	}
}

猜你喜欢

转载自blog.csdn.net/zsj777/article/details/86509101
今日推荐