淘东电商项目(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个步骤已经完成,下一篇博客继续讲解使用设计模式(策略、工厂等)根据支付方式来进行支付。

目录
相关文章
|
4月前
|
API
支付系统38-----支付宝支付---统一收单线下交易查询 第一步下单------》发起支付请求,登录,确认支付,查单接口开发,swagger接口全部呈现,
支付系统38-----支付宝支付---统一收单线下交易查询 第一步下单------》发起支付请求,登录,确认支付,查单接口开发,swagger接口全部呈现,
|
Web App开发
如何实现一个项目配置多个商户信息付款给对应商户
说明:本帖主要说明如何实现给一个平台配置多个商户的号实现多个商户收款。主要用于没有门店和第三方授权方式 支付宝最终是根据请求过来的appid来判断哪一个商户收款(也就是请求是谁的appid就收款到谁的账号下)    方案一:      1.
1371 12
|
28天前
|
缓存 NoSQL Java
京东电商下单黄金链路:防止订单重复提交与支付的深度解析
【10月更文挑战第21天】在电商领域,尤其是在像京东这样的大型电商平台中,防止订单重复提交与支付是一项至关重要的任务。
92 44
|
7天前
|
JSON API 数据格式
淘宝 / 天猫官方商品 / 订单订单 API 接口丨商品上传接口对接步骤
要对接淘宝/天猫官方商品或订单API,需先注册淘宝开放平台账号,创建应用获取App Key和App Secret。之后,详细阅读API文档,了解接口功能及权限要求,编写认证、构建请求、发送请求和处理响应的代码。最后,在沙箱环境中测试与调试,确保API调用的正确性和稳定性。
|
11天前
|
API
使用京东API接口进行支付结算有哪些注意事项?
使用京东API接口进行支付结算时,需遵守京东开放平台规定,保护用户隐私,关注API接口变化,确保应用合法、完整、可靠,正确使用API对接信息,保持API接口调用成功率,及时整改程序缺陷,结算依据以商家后台系统为准。如需帮助,请私信或评论联系。
|
3月前
|
API 开发者
淘宝官方商品、交易、订单、物流、插旗接口接入说明
这些接口涉及淘宝店铺订单管理的关键方面,包括订单列表、订单详情及订单物流信息的获取。订单列表接口(如`taobao.trades.sold.get`和`taobao.topats.trades.sold.get`)帮助商家快速了解订单概览,进行基本管理和统计。订单详情接口(如`taobao.trade.fullinfo.get`和`taobao.topats.trades.fullinfo.get`)提供单个订单的全面信息,便于发货准备和服务支持。订单物流接口则允许跟踪订单的物流状态,确保配送顺畅。使用这些接口需遵循淘宝开放平台的规定,并关注API调用限制与更新。
|
6月前
|
API 开发者
淘宝店铺订单接口丨淘宝店铺订单交易接口技术文档
淘宝店铺订单接口丨淘宝店铺订单交易接口技术文档
|
设计模式 算法 Java
淘东电商项目(58) -聚合支付(基于设计模式自动跳转支付接口)
淘东电商项目(58) -聚合支付(基于设计模式自动跳转支付接口)
79 0
|
6月前
|
监控 供应链 API
为多渠道销售集成商品API接口的正式步骤指南
摘要: 在当今的零售环境中,企业通过多渠道销售策略来扩大市场覆盖范围并提高客户接触率。商品API接口的集成是实现这一目标的关键技术手段之一。本文旨在提供一套系统的步骤指南,帮助企业高效地为其多渠道销售体系集成商品API接口。
|
数据安全/隐私保护
微信支付系列之——统一下单
微信支付系列之——统一下单
284 2