TCC-Transaction 分布式事务 —— 项目实战

本文主要基于 TCC-Transaction 1.2.3.3 正式版

  1. 概述
    本文分享 TCC 项目实战。以官方 Maven项目 tcc-transaction-http-sample 为例子( tcc-transaction-dubbo-sample 类似 )。

首先我们简单了解下这个项目。

image.png

首页 => 商品列表 => 确认支付页 => 支付结果页
使用账户余额 + 红包余额联合支付购买商品,并账户之间转账。
项目拆分三个子 Maven 项目:

tcc-transaction-http-order :商城服务,提供商品和商品订单逻辑。
tcc-transaction-http-capital :资金服务,提供账户余额逻辑。
tcc-transaction-http-redpacket :红包服务,提供红包余额逻辑。

2. 实体结构

2.1 商城服务

image.png

Shop,商店表。实体代码如下:


public class Shop {

    /**
     * 商店编号
     */
    private long id;
    /**
     * 所有者用户编号
     */
    private long ownerUserId;
}

Product,商品表。实体代码如下:

public class Product implements Serializable {

    /**
     * 商品编号
     */
    private long productId;
    /**
     * 商店编号
     */
    private long shopId;
    /**
     * 商品名
     */
    private String productName;
    /**
     * 单价
     */
    private BigDecimal price;
}

Order,订单表。实现代码如下:

public class Order implements Serializable {

    private static final long serialVersionUID = -5908730245224893590L;

    /**
     * 订单编号
     */
    private long id;
    /**
     * 支付( 下单 )用户编号
     */
    private long payerUserId;
    /**
     * 收款( 商店拥有者 )用户编号
     */
    private long payeeUserId;
    /**
     * 红包支付金额
     */
    private BigDecimal redPacketPayAmount;
    /**
     * 账户余额支付金额
     */
    private BigDecimal capitalPayAmount;
    /**
     * 订单状态
     * - DRAFT :草稿
     * - PAYING :支付中
     * - CONFIRMED :支付成功
     * - PAY_FAILED :支付失败
     */
    private String status = "DRAFT";
    /**
     * 商户订单号,使用 UUID 生成
     */
    private String merchantOrderNo;

    /**
     * 订单明细数组
     * 非存储字段
     */
    private List<OrderLine> orderLines = new ArrayList<OrderLine>();
}

OrderLine,订单明细。实体代码如下:

public class OrderLine implements Serializable {

    private static final long serialVersionUID = 2300754647209250837L;

    /**
     * 订单编号
     */
    private long id;
    /**
     * 商品编号
     */
    private long productId;
    /**
     * 数量
     */
    private int quantity;
    /**
     * 单价
     */
    private BigDecimal unitPrice;
}

业务逻辑:

下单时,插入订单状态为 “DRAFT” 的订单( Order )记录,并插入购买的商品订单明细( OrderLine )记录。支付时,更新订单状态为 “PAYING”。

订单支付成功,更新订单状态为 “CONFIRMED”。
订单支付失败,更新订单状体为 “PAY_FAILED”。
2.2 资金服务
关系较为简单,有两个实体:
CapitalAccount,资金账户余额。实体代码如下:

public class CapitalAccount {

    /**
     * 账户编号
     */
    private long id;
    /**
     * 用户编号
     */
    private long userId;
    /**
     * 余额
     */
    private BigDecimal balanceAmount;
}

TradeOrder,交易订单表。实体代码如下:

public class TradeOrder {
    /**
     * 交易订单编号
     */
    private long id;
    /**
     * 转出用户编号
     */
    private long selfUserId;
    /**
     * 转入用户编号
     */
    private long oppositeUserId;
    /**
     * 商户订单号
     */
    private String merchantOrderNo;
    /**
     * 金额
     */
    private BigDecimal amount;
    /**
     * 交易订单状态
     * - DRAFT :草稿
     * - CONFIRM :交易成功
     * - CANCEL :交易取消
     */
    private String status = "DRAFT";
}

业务逻辑:

订单支付支付中,插入交易订单状态为 “DRAFT” 的订单( TradeOrder )记录,并更新减少下单用户的资金账户余额。

订单支付成功,更新交易订单状态为 “CONFIRM”,并更新增加商店拥有用户的资金账户余额。
订单支付失败,更新交易订单状态为 “CANCEL”,并更新增加( 恢复 )下单用户的资金账户余额。
2.3 红包服务
关系较为简单,和资金服务 99.99% 相同,有两个实体:

RedPacketAccount,红包账户余额。实体代码如下:

public class RedPacketAccount {

    /**
     * 账户编号
     */
    private long id;
    /**
     * 用户编号
     */
    private long userId;
    /**
     * 余额
     */
    private BigDecimal balanceAmount;
}

TradeOrder,交易订单表。实体代码如下:

public class TradeOrder {

    /**
     * 交易订单编号
     */
    private long id;
    /**
     * 转出用户编号
     */
    private long selfUserId;
    /**
     * 转入用户编号
     */
    private long oppositeUserId;
    /**
     * 商户订单号
     */
    private String merchantOrderNo;
    /**
     * 金额
     */
    private BigDecimal amount;
    /**
     * 交易订单状态
     * - DRAFT :草稿
     * - CONFIRM :交易成功
     * - CANCEL :交易取消
     */
    private String status = "DRAFT";
}

业务逻辑:

订单支付支付中,插入交易订单状态为 “DRAFT” 的订单( TradeOrder )记录,并更新减少下单用户的红包账户余额。

订单支付成功,更新交易订单状态为 “CONFIRM”,并更新增加商店拥有用户的红包账户余额。
订单支付失败,更新交易订单状态为 “CANCEL”,并更新增加( 恢复 )下单用户的红包账户余额。

  1. 服务调用
    服务之间,通过 HTTP 进行调用。

红包服务和资金服务为商城服务提供调用( 以资金服务为例子 ):

XML 配置如下 :

// appcontext-service-provider.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:util="http://www.springframework.org/schema/util"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">

    <bean name="capitalAccountRepository"
          class="org.mengyun.tcctransaction.sample.http.capital.domain.repository.CapitalAccountRepository"/>

    <bean name="tradeOrderRepository"
          class="org.mengyun.tcctransaction.sample.http.capital.domain.repository.TradeOrderRepository"/>

    <bean name="capitalTradeOrderService"
          class="org.mengyun.tcctransaction.sample.http.capital.service.CapitalTradeOrderServiceImpl"/>

    <bean name="capitalAccountService"
          class="org.mengyun.tcctransaction.sample.http.capital.service.CapitalAccountServiceImpl"/>

    <bean name="capitalTradeOrderServiceExporter"
          class="org.springframework.remoting.httpinvoker.SimpleHttpInvokerServiceExporter">
        <property name="service" ref="capitalTradeOrderService"/>
        <property name="serviceInterface"
                  value="org.mengyun.tcctransaction.sample.http.capital.api.CapitalTradeOrderService"/>
    </bean>

    <bean name="capitalAccountServiceExporter"
          class="org.springframework.remoting.httpinvoker.SimpleHttpInvokerServiceExporter">
        <property name="service" ref="capitalAccountService"/>
        <property name="serviceInterface"
                  value="org.mengyun.tcctransaction.sample.http.capital.api.CapitalAccountService"/>
    </bean>


    <bean id="httpServer"
          class="org.springframework.remoting.support.SimpleHttpServerFactoryBean">
        <property name="contexts">
            <util:map>
                <entry key="/remoting/CapitalTradeOrderService" value-ref="capitalTradeOrderServiceExporter"/>
                <entry key="/remoting/CapitalAccountService" value-ref="capitalAccountServiceExporter"/>
            </util:map>
        </property>
        <property name="port" value="8081"/>
    </bean>

</beans>

Java对应的代码如下:

public class CapitalAccountServiceImpl implements CapitalAccountService {
    
    @Autowired
    CapitalAccountRepository capitalAccountRepository;

    @Override
    public BigDecimal getCapitalAccountByUserId(long userId) {
        return capitalAccountRepository.findByUserId(userId).getBalanceAmount();
    }

}

public class CapitalAccountServiceImpl implements CapitalAccountService {

    @Autowired
    CapitalAccountRepository capitalAccountRepository;

    @Override
    public BigDecimal getCapitalAccountByUserId(long userId) {
        return capitalAccountRepository.findByUserId(userId).getBalanceAmount();
    }

}

商城服务调用

XML 配置如下:
// appcontext-service-consumer.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="httpInvokerRequestExecutor"
          class="org.springframework.remoting.httpinvoker.CommonsHttpInvokerRequestExecutor">
        <property name="httpClient">
            <bean class="org.apache.commons.httpclient.HttpClient">
                <property name="httpConnectionManager">
                    <ref bean="multiThreadHttpConnectionManager"/>
                </property>
            </bean>
        </property>
    </bean>

    <bean id="multiThreadHttpConnectionManager"
          class="org.apache.commons.httpclient.MultiThreadedHttpConnectionManager">
        <property name="params">
            <bean class="org.apache.commons.httpclient.params.HttpConnectionManagerParams">
                <property name="connectionTimeout" value="200000"/>
                <property name="maxTotalConnections" value="600"/>
                <property name="defaultMaxConnectionsPerHost" value="512"/>
                <property name="soTimeout" value="5000"/>
            </bean>
        </property>
    </bean>

    <bean id="captialTradeOrderService" class="org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean">
        <property name="serviceUrl" value="http://localhost:8081/remoting/CapitalTradeOrderService"/>
        <property name="serviceInterface"
                  value="org.mengyun.tcctransaction.sample.http.capital.api.CapitalTradeOrderService"/>
        <property name="httpInvokerRequestExecutor" ref="httpInvokerRequestExecutor"/>
    </bean>

    <bean id="capitalAccountService" class="org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean">
        <property name="serviceUrl" value="http://localhost:8081/remoting/CapitalAccountService"/>
        <property name="serviceInterface"
                  value="org.mengyun.tcctransaction.sample.http.capital.api.CapitalAccountService"/>
        <property name="httpInvokerRequestExecutor" ref="httpInvokerRequestExecutor"/>
    </bean>

    <bean id="redPacketAccountService" class="org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean">
        <property name="serviceUrl" value="http://localhost:8082/remoting/RedPacketAccountService"/>
        <property name="serviceInterface"
                  value="org.mengyun.tcctransaction.sample.http.redpacket.api.RedPacketAccountService"/>
        <property name="httpInvokerRequestExecutor" ref="httpInvokerRequestExecutor"/>
    </bean>

    <bean id="redPacketTradeOrderService" class="org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean">
        <property name="serviceUrl" value="http://localhost:8082/remoting/RedPacketTradeOrderService"/>
        <property name="serviceInterface"
                  value="org.mengyun.tcctransaction.sample.http.redpacket.api.RedPacketTradeOrderService"/>
        <property name="httpInvokerRequestExecutor" ref="httpInvokerRequestExecutor"/>
    </bean>

</beans>

Java 接口接口如下:

public interface CapitalAccountService {
    BigDecimal getCapitalAccountByUserId(long userId);
}

public interface CapitalTradeOrderService {
    String record(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto);
}

public interface RedPacketAccountService {
    BigDecimal getRedPacketAccountByUserId(long userId);
}

public interface RedPacketTradeOrderService {
    String record(TransactionContext transactionContext, RedPacketTradeOrderDto tradeOrderDto);
}

4.下单支付流程

ps:数据访问的方法,请自己拉取代码,使用 IDE 查看.

下单支付流程,整体流程如下图


image.png

点击【支付】按钮,下单支付流程。实现代码如下:

@Controller
@RequestMapping("")
public class OrderController {
    
        @RequestMapping(value = "/placeorder", method = RequestMethod.POST)
    public ModelAndView placeOrder(@RequestParam String redPacketPayAmount,
                                   @RequestParam long shopId,
                                   @RequestParam long payerUserId,
                                   @RequestParam long productId) {
        PlaceOrderRequest request = buildRequest(redPacketPayAmount, shopId, payerUserId, productId);
        // 下单并支付订单
        String merchantOrderNo = placeOrderService.placeOrder(request.getPayerUserId(), request.getShopId(),
                request.getProductQuantities(), request.getRedPacketPayAmount());
        // 返回
        ModelAndView mv = new ModelAndView("pay_success");
        // 查询订单状态
        String status = orderService.getOrderStatusByMerchantOrderNo(merchantOrderNo);
        // 支付结果提示
        String payResultTip = null;
        if ("CONFIRMED".equals(status)) {
            payResultTip = "支付成功";
        } else if ("PAY_FAILED".equals(status)) {
            payResultTip = "支付失败";
        }
        mv.addObject("payResult", payResultTip);
        // 商品信息
        mv.addObject("product", productRepository.findById(productId));
        // 资金账户金额 和 红包账户金额
        mv.addObject("capitalAmount", accountService.getCapitalAccountByUserId(payerUserId));
        mv.addObject("redPacketAmount", accountService.getRedPacketAccountByUserId(payerUserId));
        return mv;
    }

}

调用 PlaceOrderService#placeOrder(…) 方法,下单并支付订单。
调用 OrderService#getOrderStatusByMerchantOrderNo(…) 方法,查询订单状态。
调用 PlaceOrderService#placeOrder(…) 方法,下单并支付订单。实现代码如下:

@Service
public class PlaceOrderServiceImpl {

    public String placeOrder(long payerUserId, long shopId, List<Pair<Long, Integer>> productQuantities, BigDecimal redPacketPayAmount) {
        // 获取商店
        Shop shop = shopRepository.findById(shopId);
        // 创建订单
        Order order = orderService.createOrder(payerUserId, shop.getOwnerUserId(), productQuantities);
        // 发起支付
        Boolean result = false;
        try {
            paymentService.makePayment(order, redPacketPayAmount, order.getTotalAmount().subtract(redPacketPayAmount));
        } catch (ConfirmingException confirmingException) {
            // exception throws with the tcc transaction status is CONFIRMING,
            // when tcc transaction is confirming status,
            // the tcc transaction recovery will try to confirm the whole transaction to ensure eventually consistent.
            result = true;
        } catch (CancellingException cancellingException) {
            // exception throws with the tcc transaction status is CANCELLING,
            // when tcc transaction is under CANCELLING status,
            // the tcc transaction recovery will try to cancel the whole transaction to ensure eventually consistent.
        } catch (Throwable e) {
            // other exceptions throws at TRYING stage.
            // you can retry or cancel the operation.
            e.printStackTrace();
        }
        return order.getMerchantOrderNo();
    }

}

调用 ShopRepository#findById(…) 方法,查询商店。
调用 OrderService#createOrder(…) 方法,创建订单状态为 “DRAFT” 的商城订单。实际业务不会这么做,此处仅仅是例子,简化流程。实现代码如下:

@Service
public class OrderServiceImpl {

    @Transactional
    public Order createOrder(long payerUserId, long payeeUserId, List<Pair<Long, Integer>> productQuantities) {
        Order order = orderFactory.buildOrder(payerUserId, payeeUserId, productQuantities);
        orderRepository.createOrder(order);
        return order;
    }

}

调用 PaymentService#makePayment(…) 方法,发起支付,TCC 流程。
生产代码对于异常需要进一步处理。
生产代码对于异常需要进一步处理。
生产代码对于异常需要进一步处理。

4.1 Try 阶段

商城服务

调用 PaymentService#makePayment(…) 方法,发起 Try 流程,实现代码如下:

@Compensable(confirmMethod = "confirmMakePayment", cancelMethod = "cancelMakePayment")
@Transactional
public void makePayment(Order order, BigDecimal redPacketPayAmount, BigDecimal capitalPayAmount) {
   System.out.println("order try make payment called.time seq:" + DateFormatUtils.format(Calendar.getInstance(), "yyyy-MM-dd HH:mm:ss"));
   // 更新订单状态为支付中
   order.pay(redPacketPayAmount, capitalPayAmount);
   orderRepository.updateOrder(order);
   // 资金账户余额支付订单
   String result = tradeOrderServiceProxy.record(null, buildCapitalTradeOrderDto(order));
   // 红包账户余额支付订单
   String result2 = tradeOrderServiceProxy.record(null, buildRedPacketTradeOrderDto(order));
}

设置方法注解 @Compensable

事务传播级别 Propagation.REQUIRED ( 默认值 )
设置 confirmMethod / cancelMethod 方法名
事务上下文编辑类 DefaultTransactionContextEditor ( 默认值 )
设置方法注解 @Transactional,保证方法操作原子性。

调用 OrderRepository#updateOrder(…) 方法,更新订单状态为支付中。实现代码如下:

// Order.java
public void pay(BigDecimal redPacketPayAmount, BigDecimal capitalPayAmount) {
   this.redPacketPayAmount = redPacketPayAmount;
   this.capitalPayAmount = capitalPayAmount;
   this.status = "PAYING";
}

调用 TradeOrderServiceProxy#record(…) 方法,资金账户余额支付订单。实现代码如下:

// TradeOrderServiceProxy.java
@Compensable(propagation = Propagation.SUPPORTS, confirmMethod = "record", cancelMethod = "record", transactionContextEditor = Compensable.DefaultTransactionContextEditor.class)
public String record(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto) {
   return capitalTradeOrderService.record(transactionContext, tradeOrderDto);
}

// CapitalTradeOrderService.java
public interface CapitalTradeOrderService {
    String record(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto);
}

设置方法注解 @Compensable

propagation=Propagation.SUPPORTS :支持当前事务,如果当前没有事务,就以非事务方式执行。为什么不使用 REQUIRED ?如果使用 REQUIRED 事务传播级别,事务恢复重试时,会发起新的事务。
confirmMethod、cancelMethod 使用和 try 方法相同方法名:本地发起远程服务 TCC confirm / cancel 阶段,调用相同方法进行事务的提交或回滚。远程服务的 CompensableTransactionInterceptor 会根据事务的状态是 CONFIRMING / CANCELLING 来调用对应方法。
调用 CapitalTradeOrderService#record(…) 方法,远程调用,发起资金账户余额支付订单。

本地方法调用时,参数 transactionContext 传递 null 即可,TransactionContextEditor 会设置。
远程方法调用时,参数 transactionContext 需要传递。Dubbo 远程方法调用实际也进行了传递,传递方式较为特殊,通过隐式船舱。
调用 TradeOrderServiceProxy#record(…) 方法,红包账户余额支付订单。和资金账户余额支付订单 99.99% 类似,不重复“复制粘贴”。

资金服务

调用 CapitalTradeOrderServiceImpl#record(…) 方法,红包账户余额支付订单。实现代码如下:

@Override
@Compensable(confirmMethod = "confirmRecord", cancelMethod = "cancelRecord", transactionContextEditor = Compensable.DefaultTransactionContextEditor.class)
@Transactional
public String record(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto) {
   // 调试用
   try {
       Thread.sleep(1000l);
//            Thread.sleep(10000000L);
   } catch (InterruptedException e) {
       throw new RuntimeException(e);
   }
   System.out.println("capital try record called. time seq:" + DateFormatUtils.format(Calendar.getInstance(), "yyyy-MM-dd HH:mm:ss"));
   // 生成交易订单
   TradeOrder tradeOrder = new TradeOrder(
           tradeOrderDto.getSelfUserId(),
           tradeOrderDto.getOppositeUserId(),
           tradeOrderDto.getMerchantOrderNo(),
           tradeOrderDto.getAmount()
   );
   tradeOrderRepository.insert(tradeOrder);
   // 更新减少下单用户的资金账户余额
   CapitalAccount transferFromAccount = capitalAccountRepository.findByUserId(tradeOrderDto.getSelfUserId());
   transferFromAccount.transferFrom(tradeOrderDto.getAmount());
   capitalAccountRepository.save(transferFromAccount);
   return "success";
}

设置方法注解 @Compensable

事务传播级别 Propagation.REQUIRED ( 默认值 )
设置 confirmMethod / cancelMethod 方法名
事务上下文编辑类 DefaultTransactionContextEditor ( 默认值 )
设置方法注解 @Transactional,保证方法操作原子性。

调用 TradeOrderRepository#insert(…) 方法,生成订单状态为 “DRAFT” 的交易订单。

调用 CapitalAccountRepository#save(…) 方法,更新减少下单用户的资金账户余额。Try 阶段锁定资源时,一定要先扣。TCC 是最终事务一致性,如果先添加,可能被使用。

4.2 Confirm / Cancel 阶段

当 Try 操作全部成功时,发起 Confirm 操作。
当 Try 操作存在任务失败时,发起 Cancel 操作。

4.2.1 Confirm

商城服务

调用 PaymentServiceImpl#confirmMakePayment(…) 方法,更新订单状态为支付成功。实现代码如下:

public void confirmMakePayment(Order order, BigDecimal redPacketPayAmount, BigDecimal capitalPayAmount) {
   // 调试用
   try {
       Thread.sleep(1000l);
   } catch (InterruptedException e) {
       throw new RuntimeException(e);
   }
   System.out.println("order confirm make payment called. time seq:" + DateFormatUtils.format(Calendar.getInstance(), "yyyy-MM-dd HH:mm:ss"));
   // 更新订单状态为支付成功
   order.confirm();
   orderRepository.updateOrder(order);
}

生产代码该方法需要加下 @Transactional 注解,保证原子性。
调用 OrderRepository#updateOrder(…) 方法,更新订单状态为支付成功。实现代码如下:

// Order.java
public void confirm() {
   this.status = "CONFIRMED";
}

资金服务

调用 CapitalTradeOrderServiceImpl#confirmRecord(…) 方法,更新交易订单状态为交易成功。

@Transactional
public void confirmRecord(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto) {
   // 调试用
   try {
       Thread.sleep(1000l);
   } catch (InterruptedException e) {
       throw new RuntimeException(e);
   }
   System.out.println("capital confirm record called. time seq:" + DateFormatUtils.format(Calendar.getInstance(), "yyyy-MM-dd HH:mm:ss"));
   // 查询交易记录
   TradeOrder tradeOrder = tradeOrderRepository.findByMerchantOrderNo(tradeOrderDto.getMerchantOrderNo());
   // 判断交易记录状态。因为 `#record()` 方法,可能事务回滚,记录不存在 / 状态不对
   if (null != tradeOrder && "DRAFT".equals(tradeOrder.getStatus())) {
       // 更新订单状态为交易成功
       tradeOrder.confirm();
       tradeOrderRepository.update(tradeOrder);
       // 更新增加商店拥有者用户的资金账户余额
       CapitalAccount transferToAccount = capitalAccountRepository.findByUserId(tradeOrderDto.getOppositeUserId());
       transferToAccount.transferTo(tradeOrderDto.getAmount());
       capitalAccountRepository.save(transferToAccount);
   }
}

设置方法注解 @Transactional,保证方法操作原子性。
判断交易记录状态。因为 #record() 方法,可能事务回滚,记录不存在 / 状态不对。
调用 TradeOrderRepository#update(…) 方法,更新交易订单状态为交易成功。
调用 CapitalAccountRepository#save(…) 方法,更新增加商店拥有者用户的资金账户余额。实现代码如下:

// CapitalAccount.java
public void transferTo(BigDecimal amount) {
   this.balanceAmount = this.balanceAmount.add(amount);
}

红包服务

和资源服务 99.99% 相同,不重复“复制粘贴”。

4.2.2 Cancel

商城服务

调用 PaymentServiceImpl#cancelMakePayment(…) 方法,更新订单状态为支付失败。实现代码如下:

public void cancelMakePayment(Order order, BigDecimal redPacketPayAmount, BigDecimal capitalPayAmount) {
   // 调试用
   try {
       Thread.sleep(1000l);
   } catch (InterruptedException e) {
       throw new RuntimeException(e);
   }
   System.out.println("order cancel make payment called.time seq:" + DateFormatUtils.format(Calendar.getInstance(), "yyyy-MM-dd HH:mm:ss"));
   // 更新订单状态为支付失败
   order.cancelPayment();
   orderRepository.updateOrder(order);
}

生产代码该方法需要加下 @Transactional 注解,保证原子性。
调用 OrderRepository#updateOrder(…) 方法,更新订单状态为支付失败。实现代码如下:

// Order.java
public void cancelPayment() {
    this.status = "PAY_FAILED";
}

资金服务

调用 CapitalTradeOrderServiceImpl#cancelRecord(…) 方法,更新交易订单状态为交易失败。

@Transactional
public void cancelRecord(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto) {
   // 调试用
   try {
       Thread.sleep(1000l);
   } catch (InterruptedException e) {
       throw new RuntimeException(e);
   }
   System.out.println("capital cancel record called. time seq:" + DateFormatUtils.format(Calendar.getInstance(), "yyyy-MM-dd HH:mm:ss"));
   // 查询交易记录
   TradeOrder tradeOrder = tradeOrderRepository.findByMerchantOrderNo(tradeOrderDto.getMerchantOrderNo());
   // 判断交易记录状态。因为 `#record()` 方法,可能事务回滚,记录不存在 / 状态不对
   if (null != tradeOrder && "DRAFT".equals(tradeOrder.getStatus())) {
       // / 更新订单状态为交易失败
       tradeOrder.cancel();
       tradeOrderRepository.update(tradeOrder);
       // 更新增加( 恢复 )下单用户的资金账户余额
       CapitalAccount capitalAccount = capitalAccountRepository.findByUserId(tradeOrderDto.getSelfUserId());
       capitalAccount.cancelTransfer(tradeOrderDto.getAmount());
       capitalAccountRepository.save(capitalAccount);
   }
}

设置方法注解 @Transactional,保证方法操作原子性。
判断交易记录状态。因为 #record() 方法,可能事务回滚,记录不存在 / 状态不对。
调用 TradeOrderRepository#update(…) 方法,更新交易订单状态为交易失败。
调用 CapitalAccountRepository#save(…) 方法,更新增加( 恢复 )下单用户的资金账户余额。实现代码如下:

/ CapitalAccount.java
public void cancelTransfer(BigDecimal amount) {
    transferTo(amount);
}

红包服务

和资源服务 99.99% 相同,不重复“复制粘贴”。
————————————————
版权声明:本文为CSDN博主「趁你未老」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_43253123/article/details/83277580

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容