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 代码的产生,一旦出现这种问题,往往排查起来比较费劲。这个就是很明显的自己给自己挖坑,而且自己掉进去之后,还出不来。
那这种怎么解决呢?直接往上抛,给上一层来处理即可,千万不要在事务中把异常自己 ”吃“ 掉。
-
别忘了事务是有范围的
事务范围这个东西比上面两个坑埋的更深!我之所以把这个也写上,是因为这是我之前在实际项目中遇到的,我写一个 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);
}
}