引言

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

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

  • 《淘东电商项目(52) -聚合支付开篇》
  • 《淘东电商项目(53) -银联支付案例源码分析》
  • 《淘东电商项目(54) -银联支付案例(同步与异步)》
  • 《淘东电商项目(55) -支付系统核心表设计》
  • 《淘东电商项目(56) -支付系统分布式事务的解决方案》

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

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

本文目录结构:

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是在我们选好了要购买的商品后提交。测试效果图如下:

淘东电商项目(57) -聚合支付(支付令牌接口)_时间戳_02

①来看看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来获取支付订单信息,效果图如下:

淘东电商项目(57) -聚合支付(支付令牌接口)_功能实现_03

①前端的核心代码如下,详细代码可以从我的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会员服务(如下图),下面开始来测试。

淘东电商项目(57) -聚合支付(支付令牌接口)_时间戳_04

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

淘东电商项目(57) -聚合支付(支付令牌接口)_功能实现_05

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

淘东电商项目(57) -聚合支付(支付令牌接口)_时间戳_06

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

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

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