1. 概述

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

建议你已经成功启动了该项目。如果不知道如何启动,可以先查看《TCC-Transaction 源码分析 —— 调试环境搭建》。如果再碰到问题,欢迎加微信公众号( 芋道源码 ),我会一一仔细回复。

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

  • 首页 => 商品列表 => 确认支付页 => 支付结果页

  • 使用账户余额 + 红包余额联合支付购买商品,并账户之间转账

项目拆分三个子 Maven 项目:

  • tcc-transaction-http-order :商城服务,提供商品和商品订单逻辑。

  • tcc-transaction-http-capital :资金服务,提供账户余额逻辑。

  • tcc-transaction-http-redpacket :红包服务,提供红包余额逻辑。

你行好事会因为得到赞赏而愉悦 
同理,开源项目贡献者会因为 Star 而更加有动力 
为 TCC-Transaction 点赞!传送门

2. 实体结构

2.1 商城服务

  • 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",并更新增加( 恢复 )下单用户的红包账户余额。

3. 服务调用

服务之间,通过 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">
    &lt;bean name="capitalAccountRepository"
         class="org.mengyun.tcctransaction.sample.http.capital.domain.repository.CapitalAccountRepository"/&gt;

    &lt;bean name="tradeOrderRepository"
         class="org.mengyun.tcctransaction.sample.http.capital.domain.repository.TradeOrderRepository"/&gt;

    &lt;bean name="capitalTradeOrderService"
         class="org.mengyun.tcctransaction.sample.http.capital.service.CapitalTradeOrderServiceImpl"/&gt;

    &lt;bean name="capitalAccountService"
         class="org.mengyun.tcctransaction.sample.http.capital.service.CapitalAccountServiceImpl"/&gt;

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

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


    &lt;bean id="httpServer"
         class="org.springframework.remoting.support.SimpleHttpServerFactoryBean"&gt;
       &lt;property name="contexts"&gt;
           &lt;util:map&gt;
               &lt;entry key="/remoting/CapitalTradeOrderService" value-ref="capitalTradeOrderServiceExporter"/&gt;
               &lt;entry key="/remoting/CapitalAccountService" value-ref="capitalAccountServiceExporter"/&gt;
           &lt;/util:map&gt;
       &lt;/property&gt;
       &lt;property name="port" value="8081"/&gt;
    &lt;/bean&gt;
    </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">
    &lt;bean id="httpInvokerRequestExecutor"
         class="org.springframework.remoting.httpinvoker.CommonsHttpInvokerRequestExecutor"&gt;
       &lt;property name="httpClient"&gt;
           &lt;bean class="org.apache.commons.httpclient.HttpClient"&gt;
               &lt;property name="httpConnectionManager"&gt;
                   &lt;ref bean="multiThreadHttpConnectionManager"/&gt;
               &lt;/property&gt;
           &lt;/bean&gt;
       &lt;/property&gt;
    &lt;/bean&gt;

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

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

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

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

    &lt;bean id="redPacketTradeOrderService" class="org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean"&gt;
       &lt;property name="serviceUrl" value="http://localhost:8082/remoting/RedPacketTradeOrderService"/&gt;
       &lt;property name="serviceInterface"
                 value="org.mengyun.tcctransaction.sample.http.redpacket.api.RedPacketTradeOrderService"/&gt;
       &lt;property name="httpInvokerRequestExecutor" ref="httpInvokerRequestExecutor"/&gt;
    &lt;/bean&gt;
    </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 查看。谢谢。

下单支付流程,整体流程如下图( 打开大图 ):

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

@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&lt;Pair&lt;Long, Integer&gt;&gt; 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);
    }
    • 本地方法调用时,参数 transactionContext 传递 null 即可,TransactionContextEditor 会设置。在《TCC-Transaction 源码分析 —— TCC 实现》「6.3 资源协调者拦截器」有详细解析。

    • 远程方法调用时,参数 transactionContext 需要传递。Dubbo 远程方法调用实际也进行了传递,传递方式较为特殊,通过隐式船舱,在《TCC-Transaction 源码分析 —— Dubbo 支持》「3. Dubbo 事务上下文编辑器」有详细解析。

    • propagation=Propagation.SUPPORTS :支持当前事务,如果当前没有事务,就以非事务方式执行。为什么不使用 REQUIRED ?如果使用 REQUIRED 事务传播级别,事务恢复重试时,会发起新的事务。

    • confirmMethodcancelMethod 使用和 try 方法相同方法名本地发起远程服务 TCC confirm / cancel 阶段,调用相同方法进行事务的提交或回滚。远程服务的 CompensableTransactionInterceptor 会根据事务的状态是 CONFIRMING / CANCELLING 来调用对应方法。

    • 设置方法注解 @Compensable

    • 调用 CapitalTradeOrderService#record(...) 方法,远程调用,发起资金账户余额支付订单。

  • 调用 TradeOrderServiceProxy#record(...) 方法,红包账户余额支付订单。和资金账户余额支付订单 99.99% 类似,不重复“复制粘贴”。

  • 调用 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% 相同,不重复“复制粘贴”。

666. 彩蛋


©著作权归作者所有:来自51CTO博客作者mb5ff80520dfa04的原创作品,如需转载,请注明出处,否则将追究法律责任

更多相关文章

  1. 一笔订单,但是误付了两笔钱!这种重复付款异常到底该如何解决?|原创
  2. 美团面试官:生成订单后一段时间不支付订单会自动关闭的功能该如何
  3. 锁住余额,为何还会更新异常?
  4. PHP 框架 Hyperf 实现处理超时未支付订单和延时队列
  5. 用PHP+Redis实现延迟任务 实现自动取消订单(详细教程)
  6. 订单模块数据库表解析(三)
  7. 订单模块数据库表解析(二)
  8. 订单模块数据库表解析(一)

随机推荐

  1. php的6种输出方式的区别
  2. 了解PHP文件上传相关知识
  3. PHP中static和self的区别
  4. 谈谈PHP中strlen和mb_strlen的区别
  5. php中常用的4种运行方式
  6. 了解一下PHP面向对象的相关概念
  7. 11个程序员最常犯的MySQL错误(PHP开发)
  8. php如何使用imagecopyresampled(图像处理
  9. 理解PHP中ob_flush和flush的区别
  10. PHP中的Session和Cookie