淘东电商项目(57) -聚合支付(支付令牌接口)

简介: 淘东电商项目(57) -聚合支付(支付令牌接口)

引言

本文代码已提交至Github(版本号:99a5a21d8139a9d05eb91f1298aa5565f7d513d5),有兴趣的同学可以下载来看看:https://github.com/ylw-github/taodong-shop

前面讲解了聚合支付的介绍、银联支付相关的源码分析、支付系统的表设计以及分布式系统的解决方案,有兴趣的同学可以参阅:

现在开始进入代码讲解,后续会逐步根据如下流程图,实现每一步骤的代码。本文主要讲解如下流程图的第1到第4个步骤:

本文目录结构:

l____引言

l____ 1. 提交订单功能实现(第1、2个步骤))

l____ 2. token获取支付内容功能实现(第3、4个步骤)

l____ 3. 测试

1. 提交订单功能实现(第1、2个步骤)

提交订单,请求的url为:http://localhost:8600/cratePayToken?payAmount=9999&orderId=20200513141452&userId=27&productName=江西脐橙,这个url是在我们选好了要购买的商品后提交。测试效果图如下:

①来看看Controller的代码,它的作用是用来创建token令牌,并把订单与预插入到数据库,生成待支付订单,下面使用Redis来生成token,Redis的key值为token,对应的value为数据库中订单的唯一主键:

@RestController
public class PayMentTransacTokenServiceImpl extends BaseApiService<JSONObject> implements PayMentTransacTokenService {
  @Autowired
  private PaymentTransactionMapper paymentTransactionMapper;
  @Autowired
  private GenerateToken generateToken;
  @Override
  public BaseResponse<JSONObject> cratePayToken(PayCratePayTokenDto payCratePayTokenDto) {
    String orderId = payCratePayTokenDto.getOrderId();
    if (StringUtils.isEmpty(orderId)) {
      return setResultError("订单号码不能为空!");
    }
    Long payAmount = payCratePayTokenDto.getPayAmount();
    if (payAmount == null) {
      return setResultError("金额不能为空!");
    }
    Long userId = payCratePayTokenDto.getUserId();
    if (userId == null) {
      return setResultError("userId不能为空!");
    }
    String productName = payCratePayTokenDto.getProductName();
    if (productName == null) {
      return setResultError("商品名称不能为空!");
    }
    // 2.将输入插入数据库中 待支付记录
    PaymentTransactionEntity paymentTransactionEntity = new PaymentTransactionEntity();
    paymentTransactionEntity.setOrderId(orderId);
    paymentTransactionEntity.setPayAmount(payAmount);
    paymentTransactionEntity.setUserId(userId);
    // 使用雪花算法 生成全局id
    paymentTransactionEntity.setPaymentId(SnowflakeIdUtils.nextId());
    paymentTransactionEntity.setProductName(productName);
    int result = paymentTransactionMapper.insertPaymentTransaction(paymentTransactionEntity);
    if (!toDaoResult(result)) {
      return setResultError("系统错误!");
    }
    // 获取主键id
    Long payId = paymentTransactionEntity.getId();
    if (payId == null) {
      return setResultError("系统错误!");
    }
    // 3.生成对应支付令牌
    String keyPrefix = "pay_";
    String token = generateToken.createToken(keyPrefix, payId + "");
    JSONObject dataResult = new JSONObject();
    dataResult.put("token", token);
    return setResultSuccess(dataResult);
  }
}

②这里的订单id使用雪花算法生成,附录上雪花算法工具类:

package com.ylw.common.web.core.util.twitter;
/**
 * Twitter_Snowflake<br>
 * SnowFlake的结构如下(每部分用-分开):<br>
 * 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 -
 * 000000000000 <br>
 * 1位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0<br>
 * 41位时间截(毫秒级),注意,41位时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 开始时间截)
 * 得到的值),这里的的开始时间截,一般是我们的id生成器开始使用的时间,由我们程序来指定的(如下下面程序IdWorker类的startTime属性)。
 * 41位的时间截,可以使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69<br>
 * 10位的数据机器位,可以部署在1024个节点,包括5位datacenterId和5位workerId<br>
 * 12位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号<br>
 * 加起来刚好64位,为一个Long型。<br>
 * SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高,经测试,
 * SnowFlake每秒能够产生26万ID左右。
 */
public class SnowflakeIdWorker {
  // ==============================Fields===========================================
  /** 开始时间截 (2015-01-01) */
  private final long twepoch = 1489111610226L;
  /** 机器id所占的位数 */
  private final long workerIdBits = 5L;
  /** 数据标识id所占的位数 */
  private final long dataCenterIdBits = 5L;
  /** 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */
  private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
  /** 支持的最大数据标识id,结果是31 */
  private final long maxDataCenterId = -1L ^ (-1L << dataCenterIdBits);
  /** 序列在id中占的位数 */
  private final long sequenceBits = 12L;
  /** 机器ID向左移12位 */
  private final long workerIdShift = sequenceBits;
  /** 数据标识id向左移17位(12+5) */
  private final long dataCenterIdShift = sequenceBits + workerIdBits;
  /** 时间截向左移22位(5+5+12) */
  private final long timestampLeftShift = sequenceBits + workerIdBits + dataCenterIdBits;
  /** 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) */
  private final long sequenceMask = -1L ^ (-1L << sequenceBits);
  /** 工作机器ID(0~31) */
  private long workerId;
  /** 数据中心ID(0~31) */
  private long dataCenterId;
  /** 毫秒内序列(0~4095) */
  private long sequence = 0L;
  /** 上次生成ID的时间截 */
  private long lastTimestamp = -1L;
  // ==============================Constructors=====================================
  /**
   * 构造函数
   * 
   * @param workerId
   *            工作ID (0~31)
   * @param dataCenterId
   *            数据中心ID (0~31)
   */
  public SnowflakeIdWorker(long workerId, long dataCenterId) {
    if (workerId > maxWorkerId || workerId < 0) {
      throw new IllegalArgumentException(
          String.format("workerId can't be greater than %d or less than 0", maxWorkerId));
    }
    if (dataCenterId > maxDataCenterId || dataCenterId < 0) {
      throw new IllegalArgumentException(
          String.format("dataCenterId can't be greater than %d or less than 0", maxDataCenterId));
    }
    this.workerId = workerId;
    this.dataCenterId = dataCenterId;
  }
  // ==============================Methods==========================================
  /**
   * 获得下一个ID (该方法是线程安全的)
   * 
   * @return SnowflakeId
   */
  public synchronized long nextId() {
    long timestamp = timeGen();
    // 如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
    if (timestamp < lastTimestamp) {
      throw new RuntimeException(String.format(
          "Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
    }
    // 如果是同一时间生成的,则进行毫秒内序列
    if (lastTimestamp == timestamp) {
      sequence = (sequence + 1) & sequenceMask;
      // 毫秒内序列溢出
      if (sequence == 0) {
        // 阻塞到下一个毫秒,获得新的时间戳
        timestamp = tilNextMillis(lastTimestamp);
      }
    }
    // 时间戳改变,毫秒内序列重置
    else {
      sequence = 0L;
    }
    // 上次生成ID的时间截
    lastTimestamp = timestamp;
    // 移位并通过或运算拼到一起组成64位的ID
    return ((timestamp - twepoch) << timestampLeftShift) //
        | (dataCenterId << dataCenterIdShift) //
        | (workerId << workerIdShift) //
        | sequence;
  }
  /**
   * 阻塞到下一个毫秒,直到获得新的时间戳
   * 
   * @param lastTimestamp
   *            上次生成ID的时间截
   * @return 当前时间戳
   */
  protected long tilNextMillis(long lastTimestamp) {
    long timestamp = timeGen();
    while (timestamp <= lastTimestamp) {
      timestamp = timeGen();
    }
    return timestamp;
  }
  /**
   * 返回以毫秒为单位的当前时间
   * 
   * @return 当前时间(毫秒)
   */
  protected long timeGen() {
    return System.currentTimeMillis();
  }
}

2. token获取支付内容功能实现(第3、4个步骤)

在上一步,我们获取到了token了,现在通过token来获取支付订单信息,效果图如下:

①前端的核心代码如下,详细代码可以从我的github clone代码下来看:

<div class="scent-order">
        <div class="scent-order-info">
            <strong>商品订单:</strong> <span style="color: #b7b0b0;">${data.orderId}</span>
        </div>
        <div class="scent-order-info">
            <strong>支付金额:</strong> <span
                    style="color: #0ac265; font-size: 15px;"></span><span
                    style="color: red; font-size: 24px;">
            ¥${(data.payAmount/100)?string('0.00')}</span><br />
        </div>
        <div class="scent-order-info">
            <strong>订单详情:</strong>
            <hr />
            <div class="scent-order-info-desc">
                <span>商品名称:${data.productName}</span>
            </div>
            <div class="scent-order-info-desc">
                <span>支付订单:${data.paymentId}</span>
            </div>
            <div class="scent-order-info-desc">
                <span>应付金额: ¥${(data.payAmount/100)?string('0.00')} </span>
            </div>
            <div class="scent-order-info-desc">
                <span>购买时间:${currentTime}</span>
            </div>
        </div>
    </div>

②首先请求获取订单详情,携带token参数,url为:http://localhost:8079/pay?payToken=第1、2步骤返回的token,看看Controller代码,它的流程主要是根据token获取订单详情,然后显示到index页面。

/**
 * description: 支付
 * create by: YangLinWei
 * create time: 2020/5/13 1:35 下午
 */
@Controller
public class PayController extends BaseWebController {
    @Autowired
    private PayMentTransacInfoFeign payMentTransacInfoFeign;
    @Autowired
    private PaymentChannelFeign paymentChannelFeign;
  /**
   * 跳转到index页面
   */
  private static final String INDEX_FTL = "index";
    @RequestMapping("/pay")
    public String pay(HttpServletRequest request, String payToken, Model model) {
        // 1.验证payToken参数
        if (StringUtils.isEmpty(payToken)) {
            setErrorMsg(model, "支付令牌不能为空!");
            return ERROR_500_FTL;
        }
        // 2.使用payToken查询支付信息
        BaseResponse<PayMentTransacDTO> tokenByPayMentTransac = payMentTransacInfoFeign.tokenByPayMentTransac(payToken);
        if (!isSuccess(tokenByPayMentTransac)) {
            setErrorMsg(model, tokenByPayMentTransac.getMsg());
            return ERROR_500_FTL;
        }
        // 3.查询支付信息
        PayMentTransacDTO data = tokenByPayMentTransac.getData();
        model.addAttribute("data", data);
        // 4.查询渠道信息
        List<PaymentChannelDTO> paymentChanneList = paymentChannelFeign.selectAll();
        model.addAttribute("paymentChanneList", paymentChanneList);
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        model.addAttribute("currentTime",sdf.format(new Date()));
        return INDEX_FTL;
    }
}

3. 测试

测试前须知:因为之前聚合支付模块已经实现了单点登录,所以测试前,需要启动的模块有:Eureka注册中心、xxlsso单点登录系统、member会员服务(如下图),下面开始来测试。

1.启动支付服务AppPay和聚合支付门户服务AppPortalPayWeb

2.首先验证获取token令牌,浏览器请求:http://localhost:8600/cratePayToken?payAmount=9999&orderId=20200513141452&userId=27&productName=广东米酒

3.根据返回的token请求支付申请:http://localhost:8079/pay?payToken=pay_c013d23b039446c68f522517929cfa57

好了,到此为止,支付流程图的第1到第4个步骤已经完成,下一篇博客继续讲解使用设计模式(策略、工厂等)根据支付方式来进行支付。

目录
相关文章
|
3月前
|
安全 API 开发者
转账到支付宝账户接口:一次开发,提升打款效率
转账到支付宝账户接口:一次开发,提升打款效率
41 0
|
10月前
|
前端开发 安全 数据安全/隐私保护
支付宝支付流程解读
支付宝支付流程解读
如何实现一个项目配置多个商户信息付款给对应商户
说明:本帖主要说明如何实现给一个平台配置多个商户的号实现多个商户收款。主要用于没有门店和第三方授权方式 支付宝最终是根据请求过来的appid来判断哪一个商户收款(也就是请求是谁的appid就收款到谁的账号下)    方案一:      1.
1285 0
|
4天前
|
前端开发 应用服务中间件 Android开发
10-对接支付宝当面付接口以及订单管理接口
10-对接支付宝当面付接口以及订单管理接口
9 0
|
XML 移动开发 API
微信支付开发(7) H5支付
关键字:微信支付 微信支付v3 H5支付 wap支付 prepay_id 作者:方倍工作室原文: http://www.cnblogs.com/txw1958/p/wxpayv3_h5.html    本文介绍微信支付下的H5支付实现流程。
2858 0
该商户的支付宝账号暂不支持收款,请联系商户核实信息(ALIN42276)自查方案
错误原因 这个报错原因一般是在接口中传入了seller_id参数,但是这个seller_id和调用接口的appid对应的支付宝账户的pid不一致导致  解决方案 不传入seller_id参数或是把seller_id修改为和appid对应的支付宝账户的pid来测试
1978 0
|
6月前
|
数据安全/隐私保护
微信支付系列之——统一下单
微信支付系列之——统一下单
68 2
|
7月前
|
缓存 小程序 API
从零玩转系列之微信支付实战Uni-App微信授权登录和装修下单页面和搭建下单接口以及发起下单请求2
从零玩转系列之微信支付实战Uni-App微信授权登录和装修下单页面和搭建下单接口以及发起下单请求2
96 0
|
9月前
|
小程序 安全 前端开发
从零玩转系列之微信支付实战PC端支付微信退款接口搭建1
从零玩转系列之微信支付实战PC端支付微信退款接口搭建
68 0
|
9月前
|
测试技术 API 数据库
从零玩转系列之微信支付实战PC端支付微信退款接口搭建3
从零玩转系列之微信支付实战PC端支付微信退款接口搭建
62 0