新入职的你加入了开发一组,也接到了开发任务,并且你也顺利的修复了bug,完成了快递员、司机的鉴权,现在的你已经对项目的业务功能、开发环境以及网关代码设计都有了一定的了解,但是对于用户端的校验和鉴权并没有涉及,接下来你需要完成用户端的校验和鉴权。另外,需要你了解下如何与三方支付平台对接,因为后面你需要接手支付微服务的开发。
在司机端、快递员端和管理管,登录成功后会生成jwt的token,前端将此token保存起来,当请求后端服务时,在请求头中携带此token,服务端需要对token进行校验以及鉴权操作,这种模式就是【单token模式】。
该模式存在什么问题吗?
本资源分享自 www.cx1314.cn 欢迎访问获取更多资源
其实是有问题的,主要是token有效期设置长短的问题,如果设置的比较短,用户会频繁的登录,如果设置的比较长,会不太安全,因为token一旦被黑客截取的话,就可以通过此token与服务端进行交互了。
另外一方面,token是无状态的,也就是说,服务端一旦颁发了token就无法让其失效(除非过了有效期),这样的话,如果我们检测到token异常也无法使其失效,所以这也是无状态token存在的问题。
为了解决此问题,我们将采用【双token三验证】的解决方案来解决此问题。
为了解决单token模式下存在的问题,所以我们可以通过【双token三验证】的模式进行改进实现,主要解决的两个问题如下:
详细流程如下:更多极品IT资源 www.cx1314.cn
# 4、用户端token校验与鉴权
客户端的token是采用了【双token三验证】解决方案来实现的。
首先参考前端部署文档 中的用户端部署步骤进行部署。
用户端是采用微信小程序开发的,所以需要整合小程序的登录,具体的登录流程如下:
更多内容参考微信小程序官方文档:点击查看
## 4.3、阅读代码
在sl-express-gateway
中将用户端的登录和刷新token地址设置到白名单中:
在登录接口中接收com.sl.ms.web.customer.vo.user.UserLoginRequestVO
对象,该对象中包含了【登录临时凭证】和【手机号临时凭证】,其中【手机号临时凭证】是用于用户授权获取手机号的凭证,否则获取不到手机号。
/**
* C端用户登录
*/
@Data
public class UserLoginRequestVO {
@ApiModelProperty("登录临时凭证")
private String code;
@ApiModelProperty("手机号临时凭证")
private String phoneCode;
}
Controller方法定义如下:
/**
* C端用户登录--微信登录
*
* @param userLoginRequestVO 用户登录信息
* @return 登录结果
*/
@PostMapping("/login")
@ApiOperation("登录")
public R<UserLoginVO> login(@RequestBody UserLoginRequestVO userLoginRequestVO) throws IOException {
UserLoginVO login = memberService.login(userLoginRequestVO);
return R.success(login);
}
在MemberServiceImpl实现类中,主要完对于登录业务的实现,首先根据【登录临时凭证】通过微信开放平台接口进行查询,如果用户不存在就需要通过【手机号临时凭证】查询手机号,完成用户注册。
最终通过com.sl.ms.web.customer.service.TokenService
生成token,分别生成了长短令牌。
/**
* 登录
*
* @param userLoginRequestVO 登录code
* @return 用户信息
*/
@Override
public UserLoginVO login(UserLoginRequestVO userLoginRequestVO) throws IOException {
// 1 调用微信开放平台小程序的api,根据code获取openid
JSONObject jsonObject = wechatService.getOpenid(userLoginRequestVO.getCode());
// 2 若code不正确,则获取不到openid,响应失败
if (ObjectUtil.isNotEmpty(jsonObject.getInt("errcode"))) {
throw new SLWebException(jsonObject.getStr("errmsg"));
}
String openid = jsonObject.getStr("openid");
/*
* 3 根据openid从数据库查询用户
* 3.1 如果为新用户,此处返回为null
* 3.2 如果为已经登录过的老用户,此处返回为user对象 (包含openId,phone,unionId等字段)
*/
MemberDTO user = getByOpenid(openid);
/*
* 4 构造用户数据,设置openId,unionId
* 4.1 如果user为null,则为新用户,需要构建新的user对象,并设置openId,unionId
* 4.2 如果user不为null,则为老用户,无需设置openId,unionId
*/
user = ObjectUtil.isNotEmpty(user) ? user : MemberDTO.builder()
// openId
.openId(openid)
// 平台唯一ID
.authId(jsonObject.getStr("unionid"))
.build();
// 5 调用微信开放平台小程序的api获取微信绑定的手机号
String phone = wechatService.getPhone(userLoginRequestVO.getPhoneCode());
/*
* 6 新用户绑定手机号或者老用户更新手机号
* 6.1 如果user.getPhone()为null,则为新用户,需要设置手机号,并保存数据库
* 6.2 如果user.getPhone()不为null,但是与微信获取到的手机号不一样 则表示用户改了微信绑定的手机号,需要设置手机号,并保存数据库
* 以上俩种情况,都需要重新设置手机号,并保存数据库
*/
if (ObjectUtil.notEqual(user.getPhone(), phone)) {
user.setPhone(phone);
save(user);
}
// 7 如果为新用户,查询数据库获取用户ID
if (ObjectUtil.isEmpty(user.getId())) {
user = getByOpenid(openid);
}
// 8 将用户ID存入token
Map<String, Object> claims = MapUtil.<String, Object>builder()
.put(Constants.GATEWAY.USER_ID, user.getId()).build();
// 9 封装用户信息和双token,响应结果
return UserLoginVO
.builder()
.openid(openid)
.accessToken(this.tokenService.createAccessToken(claims))
.refreshToken(this.tokenService.createRefreshToken(claims))
.binding(StatusEnum.NORMAL.getCode())
.build();
}
在TokenService中定义了3个方法,分别是:
代码实现如下:
package com.sl.ms.web.customer.service.impl;
import cn.hutool.core.date.DateField;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import com.sl.ms.web.customer.properties.JwtProperties;
import com.sl.ms.web.customer.service.TokenService;
import com.sl.ms.web.customer.vo.user.UserLoginVO;
import com.sl.transport.common.util.JwtUtils;
import com.sl.transport.common.util.ObjectUtil;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.time.Duration;
import java.util.Map;
@Service
public class TokenServiceImpl implements TokenService {
@Resource
private JwtProperties jwtProperties;
@Resource
private StringRedisTemplate stringRedisTemplate;
public static final String REDIS_REFRESH_TOKEN_PREFIX = "SL_CUSTOMER_REFRESH_TOKEN_";
@Override
public String createAccessToken(Map<String, Object> claims) {
//生成短令牌的有效期时间单位为:分钟
return JwtUtils.createToken(claims, jwtProperties.getPrivateKey(), jwtProperties.getAccessTtl(),
DateField.MINUTE);
}
@Override
public String createRefreshToken(Map<String, Object> claims) {
//生成长令牌的有效期时间单位为:小时
Integer ttl = jwtProperties.getRefreshTtl();
String refreshToken = JwtUtils.createToken(claims, jwtProperties.getPrivateKey(), ttl);
//长令牌只能使用一次,需要将其存储到redis中,变成有状态的
String redisKey = this.getRedisRefreshToken(refreshToken);
this.stringRedisTemplate.opsForValue().set(redisKey, refreshToken, Duration.ofHours(ttl));
return refreshToken;
}
@Override
public UserLoginVO refreshToken(String refreshToken) {
if (StrUtil.isEmpty(refreshToken)) {
return null;
}
Map<String, Object> originClaims = JwtUtils.checkToken(refreshToken, this.jwtProperties.getPublicKey());
if (ObjectUtil.isEmpty(originClaims)) {
//token无效
return null;
}
//通过redis校验,原token是否使用过,来确保token只能使用一次
String redisKey = this.getRedisRefreshToken(refreshToken);
Boolean bool = this.stringRedisTemplate.hasKey(redisKey);
if (ObjectUtil.notEqual(bool, Boolean.TRUE)) {
//原token过期或已经使用过
return null;
}
//删除原token
this.stringRedisTemplate.delete(redisKey);
//重新生成长短令牌
String newRefreshToken = this.createRefreshToken(originClaims);
String accessToken = this.createAccessToken(originClaims);
return UserLoginVO.builder()
.accessToken(accessToken)
.refreshToken(newRefreshToken)
.build();
}
private String getRedisRefreshToken(String refreshToken) {
//md5是为了缩短key的长度
return REDIS_REFRESH_TOKEN_PREFIX + SecureUtil.md5(refreshToken);
}
}
刷新token接收请求头中的refresh_token
参数,用此参数来刷新新的长短令牌。具体代码如下:
/**
* 刷新token,校验请求头中的长令牌,生成新的长短令牌
*
* @param refreshToken 原令牌
* @return 登录结果
*/
@PostMapping("/refresh")
@ApiOperation("刷新token")
public R<UserLoginVO> refresh(@RequestHeader(Constants.GATEWAY.REFRESH_TOKEN) String refreshToken) {
UserLoginVO loginVO = memberService.refresh(refreshToken);
if (ObjectUtil.isEmpty(loginVO)) {
return R.error("刷新token失败,请重新登录.");
}
return R.success(loginVO);
}
@Override
public UserLoginVO refresh(String refreshToken) {
return this.tokenService.refreshToken(refreshToken);
}
在网关中需要配置校验token的公钥,同样也是配置在nacos中:
有了公钥就可以对token的合法性进行校验了,具体的代码实现:
@Override
public AuthUserInfoDTO check(String token) {
// 普通用户的token没有对接权限系统,需要自定实现
// 鉴权逻辑在用户端自行实现 网关统一放行
log.info("开始解析token {}", token);
Map<String, Object> claims = JwtUtils.checkToken(token, jwtProperties.getPublicKey());
if (ObjectUtil.isEmpty(claims)) {
//token失效
return null;
}
Long userId = MapUtil.get(claims, Constants.GATEWAY.USER_ID, Long.class);
//token解析成功,放行
AuthUserInfoDTO authUserInfoDTO = new AuthUserInfoDTO();
authUserInfoDTO.setUserId(userId);
return authUserInfoDTO;
}
对于用户端只需要校验token即可,不需要鉴权。
基于小程序进行功能测试即可。
第三方支付平台是指平台提供商通过通信、计算机和信息安全技术,在商家和银行之间建立连接,从而实现消费者、金融机构以及商家之间货币支付、现金流转、资金清算、查询统计的一个平台。
国内主流的三方支付平台有:支付宝、微信支付、京东支付、银联商务、拉卡拉、快钱支付、易宝支付等。
在课程中,我们主要是会对接支付宝和微信支付。
对接支付宝支付是通过支付宝的开放平台对接的,地址:https://open.alipay.com/
通过开放平台可以基于支付宝完成各种应用开发:
我们主要关注的是API,地址:https://open.alipay.com/api
在支付API中,我们重点关注【当面付】这个API。
在当面付中,有两种支付场景,一种是【付款码支付】,另一种是【扫码支付】:
在这两种支付中,我们更关注【扫码支付】,因为在我们项目的业务场景中使用的就是【扫码支付】,业务场景是这样的:用户下单 → 快递员上门取件 → 取件成功 → 快递员出示收款二维码 → 用户拿出手机,打开支付宝APP,扫一扫进行支付
更多关于【当面付】内容请参阅开发文档。
对接支付宝,首先需要支付账号,而申请账号是有条件的:
所以,对于学生而言是比较难申请自己账号的。
支付宝为开发者提供了【沙箱环境】,沙箱环境是支付宝开放平台为开发者提供的与生产环境完全隔离的联调测试环境,开发者在沙箱环境中完成的接口调用不会对生产环境中的数据造成任何影响。
按照开发文档配置沙箱环境账号。
设置完成后的账号信息(开启公钥加密模式):
沙箱环境对应的APP,自行下载并安装到手机中使用。
参考扫码支付快速接入文档。更多极品IT资源 www.cx1314.cn
接入流程:
系统交互流程:
交易状态流程:
随着订单支付成功、退款、关闭等操作,订单交易的每一个环节 trade_status(交易状态)不同。
注意:交易成功后部分退款,交易状态仍为 TRADE_SUCCESS(交易成功),如果一直部分退款退完所有交易金额则交易状态转为 TRADE_CLOSED(交易关闭),如果未退完所有交易金额,超过有效退款时间后交易状态转为 TRADE_FINISHED(交易完成)不可退款。
对接支付宝的扫码支付,需要使用到支付宝提供的SDK,SDK有两种,分别是通用版和Easy版,其中Easy版使用起来更加的简单,我们在项目中将采用Easy版进行开发。
更多信息查看文档。
通过maven坐标导入依赖:
<dependency>
<groupId>com.alipay.sdkgroupId>
<artifactId>alipay-easysdkartifactId>
<version>${alipay.easysdk.version}version>
dependency>
import com.alipay.easysdk.factory.Factory;
import com.alipay.easysdk.factory.Factory.Payment;
import com.alipay.easysdk.kernel.Config;
import com.alipay.easysdk.kernel.util.ResponseChecker;
import com.alipay.easysdk.payment.facetoface.models.AlipayTradePrecreateResponse;
public class Main {
public static void main(String[] args) throws Exception {
// 1. 设置参数(全局只需设置一次)
Factory.setOptions(getOptions());
try {
// 2. 发起API调用(以创建当面付收款二维码为例)
AlipayTradePrecreateResponse response = Payment.FaceToFace()
.preCreate("Apple iPhone11 128G", "2234567890", "5799.00");
// 3. 处理响应或异常
if (ResponseChecker.success(response)) {
System.out.println("调用成功");
} else {
System.err.println("调用失败,原因:" + response.msg + "," + response.subMsg);
}
} catch (Exception e) {
System.err.println("调用遭遇异常,原因:" + e.getMessage());
throw new RuntimeException(e.getMessage(), e);
}
}
private static Config getOptions() {
Config config = new Config();
config.protocol = "https";
config.gatewayHost = "openapi.alipay.com";
config.signType = "RSA2";
config.appId = "<-- 请填写您的AppId,例如:2019091767145019 -->";
// 为避免私钥随源码泄露,推荐从文件中读取私钥字符串而不是写入源码中
config.merchantPrivateKey = "<-- 请填写您的应用私钥,例如:MIIEvQIBADANB ... ... -->";
//注:证书文件路径支持设置为文件系统中的路径或CLASS_PATH中的路径,优先从文件系统中加载,加载失败后会继续尝试从CLASS_PATH中加载
config.merchantCertPath = "<-- 请填写您的应用公钥证书文件路径,例如:/foo/appCertPublicKey_2019051064521003.crt -->";
config.alipayCertPath = "<-- 请填写您的支付宝公钥证书文件路径,例如:/foo/alipayCertPublicKey_RSA2.crt -->";
config.alipayRootCertPath = "<-- 请填写您的支付宝根证书文件路径,例如:/foo/alipayRootCert.crt -->";
//注:如果采用非证书模式,则无需赋值上面的三个证书路径,改为赋值如下的支付宝公钥字符串即可
// config.alipayPublicKey = "<-- 请填写您的支付宝公钥,例如:MIIBIjANBg... -->";
//可设置异步通知接收服务地址(可选)
config.notifyUrl = "<-- 请填写您的支付类接口异步通知接收服务地址,例如:https://www.test.com/callback -->";
//可设置AES密钥,调用AES加解密相关接口时需要(可选)
config.encryptKey = "<-- 请填写您的AES密钥,例如:aa4BtZ4tspm2wnXLb1ThQA== -->";
return config;
}
}
拉取【sl-express-pay】工程,地址:http://git.sl-express.com/sl/sl-express-pay.git
编写NativePayHandler
支付宝实现类,在此类中,完成与支付宝的对接,进行预下单,获取二维码链接,通过二维码生成工具生成二维码,通过沙箱版支付宝APP进行扫码支付。
package com.sl.pay.handler.alipay;
import cn.hutool.core.convert.Convert;
import cn.hutool.json.JSONUtil;
import com.alipay.easysdk.factory.Factory;
import com.alipay.easysdk.kernel.util.ResponseChecker;
import com.alipay.easysdk.payment.facetoface.models.AlipayTradePrecreateResponse;
import com.sl.pay.entity.TradingEntity;
import com.sl.pay.enums.TradingStateEnum;
import com.sl.pay.handler.NativePayHandler;
import com.sl.transport.common.exception.SLException;
import org.springframework.stereotype.Component;
/**
* 支付宝实现类
*/
@Component("aliNativePayHandler")
public class AliNativePayHandler implements NativePayHandler {
@Override
public void createDownLineTrading(TradingEntity tradingEntity) throws SLException {
// 1. 设置参数(全局只需设置一次)
Factory.setOptions(AlipayConfig.getConfig());
try {
// 2. 发起API调用(以创建当面付收款二维码为例)
AlipayTradePrecreateResponse response = Factory.Payment.FaceToFace()
.preCreate(tradingEntity.getMemo(), //订单描述
Convert.toStr(tradingEntity.getTradingOrderNo()), //交易单号
Convert.toStr(tradingEntity.getTradingAmount())); //交易金额
// 3. 处理响应或异常
if (ResponseChecker.success(response)) {
System.out.println("调用成功");
tradingEntity.setPlaceOrderMsg(response.getQrCode()); //二维码信息
tradingEntity.setPlaceOrderCode(response.getCode());
tradingEntity.setPlaceOrderJson(JSONUtil.toJsonStr(response));
tradingEntity.setTradingState(TradingStateEnum.FKZ);
} else {
System.err.println("调用失败,原因:" + response.msg + "," + response.subMsg);
}
} catch (Exception e) {
System.err.println("调用遭遇异常,原因:" + e.getMessage());
throw new RuntimeException(e.getMessage(), e);
}
}
}
编写测试用例:
测试时自行修改订单号和交易单号,如果重复会支付失败
package com.sl.pay.handler.alipay;
import com.sl.pay.entity.TradingEntity;
import com.sl.pay.handler.NativePayHandler;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
import java.math.BigDecimal;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class AliNativePayHandlerTest {
@Resource(name = "aliNativePayHandler")
NativePayHandler nativePayHandler;
@Test
void createDownLineTrading() {
TradingEntity tradingEntity = new TradingEntity();
tradingEntity.setProductOrderNo(12345L); //订单号
tradingEntity.setTradingOrderNo(11223344L); //交易单号
tradingEntity.setMemo("运费");
tradingEntity.setTradingAmount(BigDecimal.valueOf(1));
this.nativePayHandler.createDownLineTrading(tradingEntity);
System.out.println("二维码信息:" + tradingEntity.getPlaceOrderMsg());
System.out.println(tradingEntity);
}
}
测试结果:
可以看到,调用成功后会返回支付链接,下面需要将支付链接转成二维码的形式,下面通过【草料二维码】工具来生成:
在后侧可以看到生成二维码:
下面通过沙箱环境的支付宝APP进行扫码支付(支付密码默认:111111):
虽然可以通过沙箱环境进行测试,但是沙箱环境的APP查看账单并不方便,有时候查询不到数据,所以在后面的测试中建议使用正式环境,也就是神领物流项目中真实的支付宝账号信息,需要注意的是,测试时需要使用支付宝正式APP进行。
将com.sl.pay.handler.alipay.AlipayConfig#getConfig
代码的注释放开即可,同时要将沙箱环境的代码注释掉。
可以通过交易单号进行查询,需要注意的是只有通过支付宝APP扫码之后才能查询到交易单。点击查看文档
代码实现如下:
@Override
public Boolean queryTrading(TradingEntity trading) throws SLException {
//Factory使用配置
Factory.setOptions(AlipayConfig.getConfig());
AlipayTradeQueryResponse queryResponse;
try {
//调用支付宝API:通用查询支付情况
queryResponse = Factory
.Payment
.Common()
.query(String.valueOf(trading.getTradingOrderNo()));
} catch (Exception e) {
String msg = StrUtil.format("查询支付宝统一下单失败:trading = {}", trading);
log.error(msg, e);
throw new SLException(msg);
}
//修改交易单状态
trading.setResultCode(queryResponse.getCode());
trading.setResultMsg(queryResponse.getSubMsg());
trading.setResultJson(JSONUtil.toJsonStr(queryResponse));
boolean success = ResponseChecker.success(queryResponse);
//响应成功,分析交易状态
if (success) {
String tradeStatus = queryResponse.getTradeStatus();
if (StrUtil.equals(TradingConstant.ALI_TRADE_CLOSED, tradeStatus)) {
//支付取消:TRADE_CLOSED(未付款交易超时关闭,或支付完成后全额退款)
trading.setTradingState(TradingStateEnum.QXDD);
} else if (StrUtil.equalsAny(tradeStatus, TradingConstant.ALI_TRADE_SUCCESS, TradingConstant.ALI_TRADE_FINISHED)) {
// TRADE_SUCCESS(交易支付成功)
// TRADE_FINISHED(交易结束,不可退款)
trading.setTradingState(TradingStateEnum.YJS);
} else {
//非最终状态不处理,当前交易状态:WAIT_BUYER_PAY(交易创建,等待买家付款)不处理
return false;
}
return true;
}
throw new SLException(trading.getResultJson(), TradingEnum.NATIVE_QUERY_FAIL.getCode(), TradingEnum.NATIVE_QUERY_FAIL.getStatus());
}
测试用例:
@Test
void queryTrading() {
TradingEntity tradingEntity = new TradingEntity();
tradingEntity.setTradingOrderNo(11223388L); //交易单号
Boolean result = this.aliBasicPayHandler.queryTrading(tradingEntity);
System.out.println("执行是否成功:" + result);
System.out.println(tradingEntity);
}
测试结果:
执行是否成功:true
TradingEntity(openId=null, productOrderNo=null, tradingOrderNo=11223388, tradingChannel=null, tradingType=null, tradingState=QXDD, payeeName=null, payeeId=null, payerName=null, payerId=null, tradingAmount=null, refund=null, isRefund=null, resultCode=10000, resultMsg=null, resultJson={“httpBody”:“{“alipay_trade_query_response”:{“code”:“10000”,“msg”:“Success”,“buyer_logon_id”:“zha***@163.com”,“buyer_pay_amount”:“0.00”,“buyer_user_id”:“2088102229491411”,“invoice_amount”:“0.00”,“out_trade_no”:“11223388”,“point_amount”:“0.00”,“receipt_amount”:“0.00”,“send_pay_date”:“2022-12-22 15:30:10”,“total_amount”:“1.00”,“trade_no”:“2022122222001491411434343927”,“trade_status”:“TRADE_CLOSED”},“sign”:“N6pBPloZlLFG7XSE4xegTYF7OYzaN5kWEsJnUEJj822Qwz5WQRafRgDL/hMKXMiOpJ+2//oRzdktx8r9saY4r4U+bSBJ+sxaRZF0gLo3ubtyQLTTvGOf7zpIeUaMaRld8LASFaN1ZmH/2BG+qLBD0SGL7TgbTS+nOSxn3ol1+eTrPW+YYEn0bpPKPtM5mU+NdkkFUecHZH2zkgN+4OLnBfiP/QuupsRh0vV+Rz9sDj4i8Io12uQPVOHnJ88Z/rzjS77IGkp3SpII2CaiG2urzsDyJPJO3JaRln8EEtsk9JNXyq+mj8gkOXp1mUHIP2BSM+tCSWn+XqtRBSuL0L9u9Q==”}”,“code”:“10000”,“msg”:“Success”,“tradeNo”:“2022122222001491411434343927”,“outTradeNo”:“11223388”,“buyerLogonId”:“zha***@163.com”,“tradeStatus”:“TRADE_CLOSED”,“totalAmount”:“1.00”,“buyerPayAmount”:“0.00”,“pointAmount”:“0.00”,“invoiceAmount”:“0.00”,“sendPayDate”:“2022-12-22 15:30:10”,“receiptAmount”:“0.00”,“buyerUserId”:“2088102229491411”}, placeOrderCode=null, placeOrderMsg=null, placeOrderJson=null, enterpriseId=null, memo=null, qrCode=null, enableFlag=null)
用于交易创建后,用户在一定时间内未进行支付,可调用该接口直接将未付款的交易进行关闭。点击查看文档
代码实现如下:
@Override
public Boolean closeTrading(TradingEntity trading) throws SLException {
//Factory使用配置
Factory.setOptions(AlipayConfig.getConfig());
try {
//调用支付宝API:通用查询支付情况
AlipayTradeCloseResponse closeResponse = Factory
.Payment
.Common()
.close(String.valueOf(trading.getTradingOrderNo()));
boolean success = ResponseChecker.success(closeResponse);
if (success) {
trading.setTradingState(TradingStateEnum.QXDD);
return true;
}
return false;
} catch (Exception e) {
throw new SLException(TradingEnum.CLOSE_FAIL, e);
}
}
测试用例:
@Test
void closeTrading() {
TradingEntity tradingEntity = new TradingEntity();
tradingEntity.setTradingOrderNo(11223377L); //交易单号
Boolean result = this.aliBasicPayHandler.closeTrading(tradingEntity);
System.out.println("执行是否成功:" + result);
System.out.println(tradingEntity);
}
当交易发生之后一段时间内,由于买家或者卖家的原因需要退款时,卖家可以通过退款接口将支付款退还给买家,支付宝将在收到退款请求并且验证成功之后,按照退款规则将支付款按原路退到买家帐号上。点击查看文档
代码实现:
@Override
public Boolean refundTrading(RefundRecordEntity refundRecord) throws SLException {
//Factory使用配置
Factory.setOptions(AlipayConfig.getConfig());
//调用支付宝API:通用查询支付情况
AlipayTradeRefundResponse refundResponse;
try {
// 支付宝easy sdk
refundResponse = Factory
.Payment
.Common()
//扩展参数:退款单号
.optional("out_request_no", refundRecord.getRefundNo())
.refund(Convert.toStr(refundRecord.getTradingOrderNo()),
Convert.toStr(refundRecord.getRefundAmount()));
} catch (Exception e) {
String msg = StrUtil.format("调用支付宝退款接口出错!refundRecord = {}", refundRecord);
log.error(msg, e);
throw new SLException(msg, TradingEnum.NATIVE_REFUND_FAIL.getCode(), TradingEnum.NATIVE_REFUND_FAIL.getStatus());
}
refundRecord.setRefundCode(refundResponse.getCode());
refundRecord.setRefundMsg(JSONUtil.toJsonStr(refundResponse));
boolean success = ResponseChecker.success(refundResponse);
if (success) {
refundRecord.setRefundStatus(RefundStatusEnum.SUCCESS);
return true;
}
throw new SLException(refundRecord.getRefundMsg(), TradingEnum.NATIVE_REFUND_FAIL.getCode(), TradingEnum.NATIVE_REFUND_FAIL.getStatus());
}
测试用例:
@Test
void refundTrading() {
RefundRecordEntity refundRecordEntity = new RefundRecordEntity();
refundRecordEntity.setTradingOrderNo(11223388L); //交易单号
refundRecordEntity.setRefundNo(11223380L); //退款单号
refundRecordEntity.setRefundAmount(BigDecimal.valueOf(0.1)); //退款金额
Boolean result = this.aliBasicPayHandler.refundTrading(refundRecordEntity);
System.out.println("执行是否成功:" + result);
System.out.println(refundRecordEntity);
}
商户可使用该接口查询自已通过alipay.trade.refund提交的退款请求是否执行成功。点击查看文档
代码实现:
@Override
public Boolean queryRefundTrading(RefundRecordEntity refundRecord) throws SLException {
//Factory使用配置
Factory.setOptions(AlipayConfig.getConfig());
AlipayTradeFastpayRefundQueryResponse response;
try {
response = Factory.Payment.Common().queryRefund(
Convert.toStr(refundRecord.getTradingOrderNo()),
Convert.toStr(refundRecord.getRefundNo()));
} catch (Exception e) {
log.error("调用支付宝查询退款接口出错!refundRecord = {}", refundRecord, e);
throw new SLException(TradingEnum.NATIVE_REFUND_FAIL, e);
}
refundRecord.setRefundCode(response.getCode());
refundRecord.setRefundMsg(JSONUtil.toJsonStr(response));
boolean success = ResponseChecker.success(response);
if (success) {
refundRecord.setRefundStatus(RefundStatusEnum.SUCCESS);
return true;
}
throw new SLException(refundRecord.getRefundMsg(), TradingEnum.NATIVE_REFUND_FAIL.getCode(), TradingEnum.NATIVE_REFUND_FAIL.getStatus());
}
测试用例:
@Test
void queryRefundTrading() {
RefundRecordEntity refundRecordEntity = new RefundRecordEntity();
refundRecordEntity.setTradingOrderNo(11223388L); //交易单号
refundRecordEntity.setRefundNo(11223388L); //退款单号
Boolean result = this.aliBasicPayHandler.queryRefundTrading(refundRecordEntity);
System.out.println("执行是否成功:" + result);
System.out.println(refundRecordEntity);
}
微信支付也有对应的开放平台,类似支付宝。
微信支付的接口分v2和v3版本,早期使用的是v2版本,目前推荐使用v3版本,v2与v3的区别如下:更多极品IT资源 www.cx1314.cn
:::info
需要注意的是,微信并没有提供沙箱环境,只能使用正式环境的账号信息才能接口调试。
:::
微信中的扫码支付称之为【Native支付】,原理与支付宝类型。
Native支付API列表:
模块名称 | 功能列表 | 描述 |
---|---|---|
Native支付 | Native下单 | 通过本接口提交微信支付Native支付订单。 |
查询订单 | 通过此接口查询订单状态。 | |
关闭订单 | 通过此接口关闭待支付订单。 | |
Native调起支付 | 商户后台系统先调用微信支付的Native支付接口,微信后台系统返回链接参数code_url,商户后台系统将code_url值生成二维码图片,用户使用微信客户端扫码后发起支付。 | |
支付结果通知 | 微信支付通过支付通知接口将用户支付成功消息通知给商户。 | |
申请退款 | 商户可以通过该接口将支付金额退还给买家。 | |
查询单笔退款 | 提交退款申请后,通过调用该接口查询退款状态 。 | |
退款结果通知 | 微信支付通过退款通知接口将用户退款成功消息通知给商户。 | |
申请交易账单 | 商户可以通过该接口获取交易账单文件的下载地址。 | |
申请资金账单 | 商户可以通过该接口获取资金账单文件的下载地址。 | |
下载账单 | 通过申请交易/资金账单获取到download_url在该接口获取到对应的账单。 |
微信支付的接口是标准的RETful风格,同样也提供了SDK,与支付宝提供的SDK相比就简化了很多,微信支付的SDK仅仅是基于Httpclient进行了必要的封装,并没有将业务api封装进去。
通过maven坐标导入依赖:
<dependency>
<groupId>com.github.wechatpay-apiv3groupId>
<artifactId>wechatpay-apache-httpclientartifactId>
<version>${wechatpay.version}version>
dependency>
二次封装代码:
package com.sl.pay.handler.wechat;
import cn.hutool.core.net.url.UrlBuilder;
import cn.hutool.core.net.url.UrlPath;
import cn.hutool.core.net.url.UrlQuery;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.sl.pay.handler.wechat.response.WeChatResponse;
import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator;
import com.wechat.pay.contrib.apache.httpclient.cert.CertificatesManager;
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import java.io.ByteArrayInputStream;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.util.Map;
/**
* 微信支付远程调用对象
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class WechatPayHttpClient {
private String mchId; //商户号
private String appId; //应用号
private String privateKey; //私钥字符串
private String mchSerialNo; //商户证书序列号
private String apiV3Key; //V3密钥
private String domain; //请求域名
private String notifyUrl; //请求地址
public static WechatPayHttpClient get() {
//通过渠道对象转化成微信支付的client对象
return WechatPayHttpClient.builder()
.appId("wx6592a2db3f85ed25")
.domain("api.mch.weixin.qq.com")
.privateKey("-----BEGIN PRIVATE KEY-----\n" +
"MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDBHGgIh80193Gh\n" +
"dpD1LtMZfTRpcWI0fImyuBCyrd3gYb3rrsARebGcHdJsQA3mVjVqVp5ybhEZDPa4\n" +
"ecoK4Ye1hTppNpI/lmLt4/uUV/zhF5ahli7hi+116Ty6svHSbuMQBuUZeTFOwGrx\n" +
"jvofU/4pGIwh8ZvkcSnyOp9uX2177UVxDBkhgbZbJp9XF2b83vUa5eHo93CziPzn\n" +
"3hFdAlBCdTXB7DH+m0nN3Jou0szGukvq7cIgGpHku4ycKSTkIhhl9WRhN6OoSEJx\n" +
"q88MXzjkzTruc85PHN52aUTUifwg3T8Y4XqFQ61dTnEmgxeD2O6/pLdB9gLsp6yC\n" +
"GqN5Lqk7AgMBAAECggEBAL4X+WzUSbSjFS9NKNrCMjm4H1zgqTxjj6TnPkC1mGEl\n" +
"tjAHwLgzJBw62wWGdGhWWpSIGccpBBm1wjTMZpAZfF66fEpP1t1Ta6UjtGZNyvfF\n" +
"IZmE3jdWZ/WXGBnsxtFQKKKBNwrBW0Fbdqq9BQjLxLitmlxbmwrgPttcy855j6vZ\n" +
"qq4MBT1v8CtUT/gz4UWW2xWovVnmWOrRSScv7Nh0pMbRpPLkNHXrBwSSNz/keORz\n" +
"XB9JSm85wlkafa7n5/IJbdTml3A/uAgW3q3JZZQotHxQsYvD4Zb5Cnc9CPAXE5L2\n" +
"Yk877kVXZMGt5QPIVcPMj/72AMtaJT67Y0fN0RYHEGkCgYEA38BIGDY6pePgPbxB\n" +
"7N/l6Df0/OKPP0u8mqR4Q0aQD3VxeGiZUN1uWXEFKsKwlOxLfIFIFk1/6zQeC0xe\n" +
"tNTKk0gTL8hpMUTNkE7vI9gFWws2LY6DE86Lm0bdFEIwh6d7Fr7zZtyQKPzMsesC\n" +
"3XV9sdSUExEi5o/VwAyf+xZlOXcCgYEA3PGZYlILjg3esPNkhDz2wxFw432i8l/B\n" +
"CPD8ZtqIV9eguu4fVtFYcUVfawBb0T11RamJkc4eiSOqayC+2ehgb+GyRLJNK4Fq\n" +
"bFcsIT+CK0HlscZw51jrMR0MxTc4RzuOIMoYDeZqeGB6/YnNyG4pw2sD8bIwHm84\n" +
"06gtJsX/v10CgYAo8g3/aEUZQHcztPS3fU2cTkkl0ev24Ew2XGypmwsX2R0XtMSB\n" +
"uNPNyFHyvkgEKK2zrhDcC/ihuRraZHJcUyhzBViFgP5HBtk7VEaM36YzP/z9Hzw7\n" +
"bqu7kZ85atdoq6xpwC3Yn/o9le17jY8rqamD1mv2hUdGvAGYsHbCQxnpBwKBgHTk\n" +
"eaMUBzr7yZLS4p435tHje1dQVBJpaKaDYPZFrhbTZR0g+IGlNmaPLmFdCjbUjiPy\n" +
"A2+Znnwt227cHz0IfWUUAo3ny3419QkmwZlBkWuzbIO2mms7lwsf9G6uvV6qepKM\n" +
"eVd5TWEsokVbT/03k27pQmfwPxcK/wS0GFdIL/udAoGAOYdDqY5/aadWCyhzTGI6\n" +
"qXPLvC+fsJBPhK2RXyc+jYV0KmrEv4ewxlK5NksuFsNkyB7wlI1oMCa/xB3T/2vT\n" +
"BALgGFPi8BJqceUjtnTYtI4R2JIVEl08RtEJwyU5JZ2rvWcilsotVZYwfuLZ9Kfd\n" +
"hkTrgNxlp/KKkr+UuKce4Vs=\n" +
"-----END PRIVATE KEY-----\n")
.mchId("1561414331")
.mchSerialNo("25FBDE3EFD31B03A4377EB9A4A47C517969E6620")
.apiV3Key("CZBK51236435wxpay435434323FFDuv3")
.notifyUrl("https://www.itcast.cn/")
.build();
}
/***
* 构建CloseableHttpClient远程请求对象
* @return org.apache.http.impl.client.CloseableHttpClient
*/
public CloseableHttpClient createHttpClient() throws Exception {
// 加载商户私钥(privateKey:私钥字符串)
PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(new ByteArrayInputStream(privateKey.getBytes(StandardCharsets.UTF_8)));
// 加载平台证书(mchId:商户号,mchSerialNo:商户证书序列号,apiV3Key:V3密钥)
PrivateKeySigner privateKeySigner = new PrivateKeySigner(mchSerialNo, merchantPrivateKey);
WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner);
// 向证书管理器增加需要自动更新平台证书的商户信息
CertificatesManager certificatesManager = CertificatesManager.getInstance();
certificatesManager.putMerchant(mchId, wechatPay2Credentials, apiV3Key.getBytes(StandardCharsets.UTF_8));
// 初始化httpClient
return com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder.create()
.withMerchant(mchId, mchSerialNo, merchantPrivateKey)
.withValidator(new WechatPay2Validator(certificatesManager.getVerifier(mchId)))
.build();
}
/***
* 支持post请求的远程调用
*
* @param apiPath api地址
* @param params 携带请求参数
* @return 返回字符串
*/
public WeChatResponse doPost(String apiPath, Map<String, Object> params) throws Exception {
String url = StrUtil.format("https://{}{}", this.domain, apiPath);
HttpPost httpPost = new HttpPost(url);
httpPost.addHeader("Accept", "application/json");
httpPost.addHeader("Content-type", "application/json; charset=utf-8");
String body = JSONUtil.toJsonStr(params);
httpPost.setEntity(new StringEntity(body, CharsetUtil.UTF_8));
CloseableHttpResponse response = this.createHttpClient().execute(httpPost);
return new WeChatResponse(response);
}
/***
* 支持get请求的远程调用
* @param apiPath api地址
* @param params 在路径中请求的参数
* @return 返回字符串
*/
public WeChatResponse doGet(String apiPath, Map<String, Object> params) throws Exception {
URI uri = UrlBuilder.create()
.setHost(this.domain)
.setScheme("https")
.setPath(UrlPath.of(apiPath, CharsetUtil.CHARSET_UTF_8))
.setQuery(UrlQuery.of(params))
.setCharset(CharsetUtil.CHARSET_UTF_8)
.toURI();
return this.doGet(uri);
}
/***
* 支持get请求的远程调用
* @param apiPath api地址
* @return 返回字符串
*/
public WeChatResponse doGet(String apiPath) throws Exception {
URI uri = UrlBuilder.create()
.setHost(this.domain)
.setScheme("https")
.setPath(UrlPath.of(apiPath, CharsetUtil.CHARSET_UTF_8))
.setCharset(CharsetUtil.CHARSET_UTF_8)
.toURI();
return this.doGet(uri);
}
private WeChatResponse doGet(URI uri) throws Exception {
HttpGet httpGet = new HttpGet(uri);
httpGet.addHeader("Accept", "application/json");
CloseableHttpResponse response = this.createHttpClient().execute(httpGet);
return new WeChatResponse(response);
}
}
代码说明:
get()
方法查询配置信息,最后封装到WechatPayHttpClient
对象中。createHttpClient()
方法封装了请求微信接口必要的参数,最后返回CloseableHttpClient
对象。doGet()、doPost()
方便对微信接口进行调用。编写WechatNativePayHandler,同样也是实现NativePayHandler接口,主要是对接微信支付的扫码支付。
package com.sl.pay.handler.wechat;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.json.JSONUtil;
import com.sl.pay.entity.TradingEntity;
import com.sl.pay.enums.TradingEnum;
import com.sl.pay.enums.TradingStateEnum;
import com.sl.pay.handler.NativePayHandler;
import com.sl.pay.handler.wechat.response.WeChatResponse;
import com.sl.transport.common.exception.SLException;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* 微信二维码支付
*/
@Component("wechatNativePayHandler")
public class WechatNativePayHandler implements NativePayHandler {
@Override
public void createDownLineTrading(TradingEntity tradingEntity) throws SLException {
// 查询配置
WechatPayHttpClient client = WechatPayHttpClient.get();
//请求地址
String apiPath = "/v3/pay/transactions/native";
//请求参数
Map<String, Object> params = MapUtil.<String, Object>builder()
.put("mchid", client.getMchId())
.put("appid", client.getAppId())
.put("description", tradingEntity.getMemo())
.put("notify_url", client.getNotifyUrl())
.put("out_trade_no", Convert.toStr(tradingEntity.getTradingOrderNo()))
.put("amount", MapUtil.<String, Object>builder()
.put("total", Convert.toInt(NumberUtil.mul(tradingEntity.getTradingAmount(), 100))) //金额,单位:分
.put("currency", "CNY") //人民币
.build())
.build();
try {
WeChatResponse response = client.doPost(apiPath, params);
if (!response.isOk()) {
//下单失败
throw new SLException(TradingEnum.NATIVE_PAY_FAIL);
}
//指定统一下单code
tradingEntity.setPlaceOrderCode(Convert.toStr(response.getStatus()));
//二维码需要展现的信息
tradingEntity.setPlaceOrderMsg(JSONUtil.parseObj(response.getBody()).getStr("code_url"));
//指定统一下单json字符串
tradingEntity.setPlaceOrderJson(JSONUtil.toJsonStr(response));
//指定交易状态
tradingEntity.setTradingState(TradingStateEnum.FKZ);
} catch (Exception e) {
throw new SLException(TradingEnum.NATIVE_PAY_FAIL);
}
}
}
测试用例:
package com.sl.pay.handler.wechat;
import com.sl.pay.entity.TradingEntity;
import com.sl.pay.handler.NativePayHandler;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
import java.math.BigDecimal;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class WechatNativePayHandlerTest {
@Resource(name = "wechatNativePayHandler")
NativePayHandler nativePayHandler;
@Test
void createDownLineTrading() {
TradingEntity tradingEntity = new TradingEntity();
tradingEntity.setProductOrderNo(12345L); //订单号
tradingEntity.setTradingOrderNo(11223388L); //交易单号
tradingEntity.setMemo("运费");
tradingEntity.setTradingAmount(BigDecimal.valueOf(1));
this.nativePayHandler.createDownLineTrading(tradingEntity);
System.out.println("二维码信息:" + tradingEntity.getPlaceOrderMsg());
System.out.println(tradingEntity);
}
}
与支付宝类型,微信支付也有查询交易单、退款等操作,同样也是实现了BasicPayHandler
接口。
package com.sl.pay.handler.wechat;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.sl.pay.constant.TradingConstant;
import com.sl.pay.entity.RefundRecordEntity;
import com.sl.pay.entity.TradingEntity;
import com.sl.pay.enums.RefundStatusEnum;
import com.sl.pay.enums.TradingStateEnum;
import com.sl.pay.handler.BasicPayHandler;
import com.sl.pay.handler.wechat.response.WeChatResponse;
import com.sl.transport.common.exception.SLException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.time.temporal.ChronoUnit;
import java.util.Map;
import static com.sl.pay.enums.TradingEnum.*;
/**
* 微信基础支付功能的实现
*/
@Slf4j
@Component("weChatBasicPayHandler")
public class WeChatBasicPayHandler implements BasicPayHandler {
@Override
public Boolean queryTrading(TradingEntity trading) throws SLException {
// 获取微信支付的client对象
WechatPayHttpClient client = WechatPayHttpClient.get();
//请求地址
String apiPath = StrUtil.format("/v3/pay/transactions/out-trade-no/{}", trading.getTradingOrderNo());
//请求参数
Map<String, Object> params = MapUtil.<String, Object>builder()
.put("mchid", client.getMchId())
.build();
WeChatResponse response;
try {
response = client.doGet(apiPath, params);
} catch (Exception e) {
log.error("调用微信接口出错!apiPath = {}, params = {}", apiPath, JSONUtil.toJsonStr(params), e);
throw new SLException(NATIVE_REFUND_FAIL, e);
}
if (response.isOk()) {
JSONObject jsonObject = JSONUtil.parseObj(response.getBody());
// 交易状态,枚举值:
// SUCCESS:支付成功
// REFUND:转入退款
// NOTPAY:未支付
// CLOSED:已关闭
// REVOKED:已撤销(仅付款码支付会返回)
// USERPAYING:用户支付中(仅付款码支付会返回)
// PAYERROR:支付失败(仅付款码支付会返回)
String tradeStatus = jsonObject.getStr("trade_state");
if (StrUtil.equalsAny(tradeStatus, TradingConstant.WECHAT_TRADE_CLOSED, TradingConstant.WECHAT_TRADE_REVOKED)) {
trading.setTradingState(TradingStateEnum.QXDD);
} else if (StrUtil.equalsAny(tradeStatus, TradingConstant.WECHAT_REFUND_SUCCESS, TradingConstant.WECHAT_TRADE_REFUND)) {
trading.setTradingState(TradingStateEnum.YJS);
} else if (StrUtil.equalsAny(tradeStatus, TradingConstant.WECHAT_TRADE_NOTPAY)) {
//如果是未支付,需要判断下时间,超过2小时未知的订单需要关闭订单以及设置状态为QXDD
long between = LocalDateTimeUtil.between(trading.getCreated(), LocalDateTimeUtil.now(), ChronoUnit.HOURS);
if (between >= 2) {
return this.closeTrading(trading);
}
} else {
//非最终状态不处理
return false;
}
//修改交易单状态
trading.setResultCode(tradeStatus);
trading.setResultMsg(jsonObject.getStr("trade_state_desc"));
trading.setResultJson(response.getBody());
return true;
}
throw new SLException(response.getBody(), NATIVE_REFUND_FAIL.getCode(), NATIVE_REFUND_FAIL.getCode());
}
@Override
public Boolean closeTrading(TradingEntity trading) throws SLException {
// 获取微信支付的client对象
WechatPayHttpClient client = WechatPayHttpClient.get();
//请求地址
String apiPath = StrUtil.format("/v3/pay/transactions/out-trade-no/{}/close", trading.getTradingOrderNo());
//请求参数
Map<String, Object> params = MapUtil.<String, Object>builder()
.put("mchid", client.getMchId())
.build();
try {
WeChatResponse response = client.doPost(apiPath, params);
if (response.getStatus() == 204) {
trading.setTradingState(TradingStateEnum.QXDD);
return true;
}
return false;
} catch (Exception e) {
throw new SLException(CLOSE_FAIL, e);
}
}
@Override
public Boolean refundTrading(RefundRecordEntity refundRecord) throws SLException {
// 获取微信支付的client对象
WechatPayHttpClient client = WechatPayHttpClient.get();
//请求地址
String apiPath = "/v3/refund/domestic/refunds";
//请求参数
Map<String, Object> params = MapUtil.<String, Object>builder()
.put("out_refund_no", Convert.toStr(refundRecord.getRefundNo()))
.put("out_trade_no", Convert.toStr(refundRecord.getTradingOrderNo()))
.put("amount", MapUtil.<String, Object>builder()
.put("refund", NumberUtil.mul(refundRecord.getRefundAmount(), 100)) //本次退款金额
.put("total", NumberUtil.mul(refundRecord.getTotal(), 100)) //原订单金额
.put("currency", "CNY") //币种
.build())
.build();
WeChatResponse response;
try {
response = client.doPost(apiPath, params);
} catch (Exception e) {
log.error("调用微信接口出错!apiPath = {}, params = {}", apiPath, JSONUtil.toJsonStr(params), e);
throw new SLException(NATIVE_REFUND_FAIL, e);
}
refundRecord.setRefundCode(Convert.toStr(response.getStatus()));
refundRecord.setRefundMsg(response.getBody());
if (response.isOk()) {
JSONObject jsonObject = JSONUtil.parseObj(response.getBody());
// SUCCESS:退款成功
// CLOSED:退款关闭
// PROCESSING:退款处理中
// ABNORMAL:退款异常
String status = jsonObject.getStr("status");
if (StrUtil.equals(status, TradingConstant.WECHAT_REFUND_PROCESSING)) {
refundRecord.setRefundStatus(RefundStatusEnum.SENDING);
} else if (StrUtil.equals(status, TradingConstant.WECHAT_REFUND_SUCCESS)) {
refundRecord.setRefundStatus(RefundStatusEnum.SUCCESS);
} else {
refundRecord.setRefundStatus(RefundStatusEnum.FAIL);
}
return true;
}
throw new SLException(refundRecord.getRefundMsg(), NATIVE_REFUND_FAIL.getCode(), NATIVE_REFUND_FAIL.getStatus());
}
@Override
public Boolean queryRefundTrading(RefundRecordEntity refundRecord) throws SLException {
// 获取微信支付的client对象
WechatPayHttpClient client = WechatPayHttpClient.get();
//请求地址
String apiPath = StrUtil.format("/v3/refund/domestic/refunds/{}", refundRecord.getRefundNo());
WeChatResponse response;
try {
response = client.doGet(apiPath);
} catch (Exception e) {
log.error("调用微信接口出错!apiPath = {}", apiPath, e);
throw new SLException(NATIVE_QUERY_REFUND_FAIL, e);
}
refundRecord.setRefundCode(Convert.toStr(response.getStatus()));
refundRecord.setRefundMsg(response.getBody());
if (response.isOk()) {
JSONObject jsonObject = JSONUtil.parseObj(response.getBody());
// SUCCESS:退款成功
// CLOSED:退款关闭
// PROCESSING:退款处理中
// ABNORMAL:退款异常
String status = jsonObject.getStr("status");
if (StrUtil.equals(status, TradingConstant.WECHAT_REFUND_PROCESSING)) {
refundRecord.setRefundStatus(RefundStatusEnum.SENDING);
} else if (StrUtil.equals(status, TradingConstant.WECHAT_REFUND_SUCCESS)) {
refundRecord.setRefundStatus(RefundStatusEnum.SUCCESS);
} else {
refundRecord.setRefundStatus(RefundStatusEnum.FAIL);
}
return true;
}
throw new SLException(response.getBody(), NATIVE_QUERY_REFUND_FAIL.getCode(), NATIVE_QUERY_REFUND_FAIL.getStatus());
}
}
测试用例:
package com.sl.pay.handler.wechat;
import com.sl.pay.entity.RefundRecordEntity;
import com.sl.pay.entity.TradingEntity;
import com.sl.pay.handler.BasicPayHandler;
import com.sl.pay.handler.alipay.AliBasicPayHandler;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@SpringBootTest
class WechatBasicPayHandlerTest {
@Resource(name = "weChatBasicPayHandler")
BasicPayHandler basicPayHandler;
@Test
void queryTrading() {
TradingEntity tradingEntity = new TradingEntity();
tradingEntity.setTradingOrderNo(11223388L); //交易单号
tradingEntity.setCreated(LocalDateTime.now());
Boolean result = this.basicPayHandler.queryTrading(tradingEntity);
System.out.println("执行是否成功:" + result);
System.out.println(tradingEntity);
}
@Test
void closeTrading() {
TradingEntity tradingEntity = new TradingEntity();
tradingEntity.setTradingOrderNo(11223377L); //交易单号
Boolean result = this.basicPayHandler.closeTrading(tradingEntity);
System.out.println("执行是否成功:" + result);
System.out.println(tradingEntity);
}
@Test
void refundTrading() {
RefundRecordEntity refundRecordEntity = new RefundRecordEntity();
refundRecordEntity.setTradingOrderNo(11223388L); //交易单号
refundRecordEntity.setRefundNo(11223380L); //退款单号
refundRecordEntity.setRefundAmount(BigDecimal.valueOf(0.1)); //退款金额
refundRecordEntity.setTotal(BigDecimal.valueOf(1)); //原金额
Boolean result = this.basicPayHandler.refundTrading(refundRecordEntity);
System.out.println("执行是否成功:" + result);
System.out.println(refundRecordEntity);
}
@Test
void queryRefundTrading() {
RefundRecordEntity refundRecordEntity = new RefundRecordEntity();
refundRecordEntity.setTradingOrderNo(11223388L); //交易单号
refundRecordEntity.setRefundNo(11223388L); //退款单号
Boolean result = this.basicPayHandler.queryRefundTrading(refundRecordEntity);
System.out.println("执行是否成功:" + result);
System.out.println(refundRecordEntity);
}
}
想象一下这样的场景,快递员提交了支付请求,由于网络等原因一直没有返回二维码,此时快递员针对该订单又发起了一次请求,这样的话就可能针对于一个订单生成了2个交易单,这样就重复了,所以我们需要在处理请求生成交易单时对该订单锁定,如果获取到锁就执行,否则就抛出异常。
实际上,在这里我们是需要使用分布式锁来实现,首先要解释下为什么是用分布式锁,不是用本地锁,是因为微服务在生产部署时一般都是集群的,而我们需要的在多个节点之间锁定,并不是在一个节点内锁定,所以就要用到分布式锁,如何实现分布式锁呢,下面我们一起来学习下。
实现分布式锁,可以借助redis的SETNX命令完成,该命令设置值时,如果key不存在,为key设置指定的值,返回1,如果存在返回0,也就意味着相同的key只能设置成功一次,假设有多个线程同时设置值,只能有一个设置成功,这样就得到互斥的效果,也就可以达到锁的效果。
192.168.150.101:0>SETNX abc 123
"1" ---设置成功
192.168.150.101:0>SETNX abc 123
"0" ---设置失败
192.168.150.101:0>SETNX abc 123
"0" ---设置失败
192.168.150.101:0>get abc
"123" ---可以正常查询值
这里举个例子,商品服务的并发操作:更多极品IT资源 www.cx1314.cn
下面我们基于并发创建交易这样的业务场景进行测试。
package com.sl.pay.lock;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.IdUtil;
import com.sl.pay.entity.TradingEntity;
import com.sl.pay.handler.NativePayHandler;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Service
public class NativePayService {
@Resource(name = "aliNativePayHandler")
private NativePayHandler nativePayHandler;
/**
* 创建交易单示例代码
*
* @param productOrderNo 订单号
* @return 交易单对象
*/
public TradingEntity createDownLineTrading(Long productOrderNo) {
TradingEntity tradingEntity = new TradingEntity();
tradingEntity.setProductOrderNo(productOrderNo);
//基于订单创建交易单
tradingEntity.setTradingOrderNo(IdUtil.getSnowflakeNextId());
tradingEntity.setCreated(LocalDateTime.now());
tradingEntity.setTradingAmount(BigDecimal.valueOf(1));
tradingEntity.setMemo("运费");
//调用三方支付创建交易
this.nativePayHandler.createDownLineTrading(tradingEntity);
return tradingEntity;
}
}
使用多线程模拟并发测试:
package com.sl.pay.lock;
import com.sl.pay.entity.TradingEntity;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
@SpringBootTest
class NativePayServiceTest {
@Resource
NativePayService nativePayService;
@Test
void createDownLineTrading() throws Exception {
Long productOrderNo = 1122334455L;
//多线程模拟并发
for (int i = 0; i < 3; i++) {
new Thread(() -> {
TradingEntity tradingEntity = nativePayService.createDownLineTrading(productOrderNo);
System.out.println("交易单:" + tradingEntity + ", 线程id = " + Thread.currentThread().getId());
}).start();
}
//睡眠20秒,等待所有子线程的完成
Thread.sleep(20000);
}
}
运行结果:
可见,对同一个订单号创建了多个交易单对象,这就是并发常见下的数据重复问题。
定义锁接口:
package com.sl.pay.lock;
public interface ILock {
/**
* 尝试获取锁
*
* @param name 锁的名称
* @param timeoutSec 锁持有的超时时间,过期后自动释放
* @return true表示获取锁成功,false表示获取锁失败
*/
boolean tryLock(String name, Long timeoutSec);
/**
* 释放锁
*/
void unlock(String name);
}
基本的实现:
package com.sl.pay.lock;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
@Component
public class SimpleRedisLock implements ILock {
@Resource
private StringRedisTemplate stringRedisTemplate;
private static final String KEY_PREFIX = "lock:";
@Override
public boolean tryLock(String name, Long timeoutSec) {
// 获取线程标示
String threadId = Thread.currentThread().getId() + "";
// 获取锁 setIfAbsent()是SETNX命令在java中的体现
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
@Override
public void unlock(String name) {
//通过del删除锁
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}
业务中使用(createDownLineTradingLock()方法):
package com.sl.pay.lock;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.IdUtil;
import com.sl.pay.entity.TradingEntity;
import com.sl.pay.handler.NativePayHandler;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Service
public class NativePayService {
@Resource(name = "aliNativePayHandler")
private NativePayHandler nativePayHandler;
@Resource
private SimpleRedisLock simpleRedisLock;
/**
* 创建交易单示例代码
*
* @param productOrderNo 订单号
* @return 交易单对象
*/
public TradingEntity createDownLineTrading(Long productOrderNo) {
TradingEntity tradingEntity = new TradingEntity();
tradingEntity.setProductOrderNo(productOrderNo);
//基于订单创建交易单
tradingEntity.setTradingOrderNo(IdUtil.getSnowflakeNextId());
tradingEntity.setCreated(LocalDateTime.now());
tradingEntity.setTradingAmount(BigDecimal.valueOf(1));
tradingEntity.setMemo("运费");
//调用三方支付创建交易
this.nativePayHandler.createDownLineTrading(tradingEntity);
return tradingEntity;
}
/**
* 创建交易单示例代码
*
* @param productOrderNo 订单号
* @return 交易单对象
*/
public TradingEntity createDownLineTradingLock(Long productOrderNo) {
//获取锁
String lockName = Convert.toStr(productOrderNo);
boolean lock = this.simpleRedisLock.tryLock(lockName, 5L);
if (!lock) {
System.out.println("没有获取到锁,线程id = " + Thread.currentThread().getId());
return null;
}
System.out.println("获取到锁,线程id = " + Thread.currentThread().getId());
TradingEntity tradingEntity = createDownLineTrading(productOrderNo);
//释放锁
this.simpleRedisLock.unlock(lockName);
return tradingEntity;
}
}
测试:
@Test
void createDownLineTradingLock() throws Exception {
Long productOrderNo = 1122334455L;
//多线程模拟并发
for (int i = 0; i < 3; i++) {
new Thread(() -> {
TradingEntity tradingEntity = nativePayService.createDownLineTradingLock(productOrderNo);
System.out.println("交易单:" + tradingEntity + ", 线程id = " + Thread.currentThread().getId());
}).start();
}
//睡眠20秒,等待所有子线程的完成
Thread.sleep(20000);
}
测试结果:
可以看到,线程23、24没有获取到锁,只要线程25获取到了锁,最终一个订单只会对应一个交易单,这样才符合需求。
自己基于Redis实现基本上是ok的,但是仔细分析会发现一些问题,比如:设置持有锁的时间为5秒,而程序所运行的时间大于5秒,这样就会出现,程序还没结束锁已经释放了,其他线程就可以获取到这个锁,而当前线程在释放锁时,就会把其他线程的锁删除了,最终可能会导致脏数据。
为了解决这个问题,我们可以在删除时判断一下,看是存储的值是否是当前线程的id,是就删除,不是就不删除。
代码实现:
@Override
public void unlock(String name) {
// 获取线程标示
String threadId = Thread.currentThread().getId() + "";
// 获取锁中的标示
String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
// 判断标示是否一致
if(threadId.equals(id)) {
// 释放锁
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}
下图展现了多线时间之间获取锁以及释放锁的过程:
这样是不是就没问题了呢?并不是,其实还是存在问题的。
问题就是,释放锁时查询与删除并不是一个原子性操作,这样带来的问题就是,查询时有数据,删除时数据可能被其他线程删除了。
除了这个问题外还有其他问题:
总结一句话,就是自己基于Redis实现分布式锁需要解决的问题非常多,实现非常的复杂,而Redisson已经完美的实现并且解决了这些问题,我们可以直接使用。
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。
官网地址:
GitHub地址:
导入依赖:
<dependency>
<groupId>org.redissongroupId>
<artifactId>redissonartifactId>
dependency>
配置:
package com.sl.pay.config;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.StrUtil;
import lombok.Data;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.redisson.config.SingleServerConfig;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.Resource;
@Data
@Configuration
@EnableConfigurationProperties(RedisProperties.class)
public class RedissonConfiguration {
@Resource
private RedisProperties redisProperties;
@Bean
public RedissonClient redissonSingle() {
Config config = new Config();
SingleServerConfig serverConfig = config.useSingleServer()
.setAddress("redis://" + redisProperties.getHost() + ":" + redisProperties.getPort());
if (null != (redisProperties.getTimeout())) {
//设置持有时间
serverConfig.setTimeout(1000 * Convert.toInt(redisProperties.getTimeout().getSeconds()));
}
if (StrUtil.isNotEmpty(redisProperties.getPassword())) {
//设置密码
serverConfig.setPassword(redisProperties.getPassword());
}
//创建RedissonClient
return Redisson.create(config);
}
}
项目中使用:
@Resource
private RedissonClient redissonClient;
/**
* 创建交易单示例代码
*
* @param productOrderNo 订单号
* @return 交易单对象
*/
public TradingEntity createDownLineTradingRedissonLock(Long productOrderNo) {
//获取锁
String lockName = Convert.toStr(productOrderNo);
//获取公平锁,优先分配给先发出请求的线程
RLock lock = redissonClient.getFairLock(lockName);
try {
//尝试获取锁,最长等待获取锁的时间为5秒
if (lock.tryLock(5L, TimeUnit.SECONDS)) {
System.out.println("获取到锁,线程id = " + Thread.currentThread().getId());
//休眠5s目的是让线程执行慢一些,容易测试出并发效果
Thread.sleep(5000);
return createDownLineTrading(productOrderNo);
}
System.out.println("没有获取到锁,线程id = " + Thread.currentThread().getId());
} catch (Exception e) {
e.printStackTrace();
} finally {
//释放锁,需要判断当前线程是否获取到锁
if (lock.isLocked() && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
return null;
}
测试用例:
@Test
void createDownLineTradingRedissonLock() throws Exception {
Long productOrderNo = 1122334455L;
//多线程模拟并发
for (int i = 0; i < 3; i++) {
new Thread(() -> {
TradingEntity tradingEntity = nativePayService.createDownLineTradingRedissonLock(productOrderNo);
System.out.println("交易单:" + tradingEntity + ", 线程id = " + Thread.currentThread().getId());
}).start();
}
//睡眠20秒,等待所有子线程的完成
Thread.sleep(20000);
}
测试结果:
可以看到,与我们自己实现的效果是一样的,可见使用Redisson是非常方便实现分布式锁的。
在使用Redisson分布式锁时,我们有没有指定存储到Redis中锁的有效期时间?如果有的话是多久?如果程序执行时间超出这个时间会怎么样?
其实,在程序中我们并没有指定存储到Redis中锁的有效期时间,而是Redisson的默认存储时间,默认时间是30秒。如果程序的执行时间超出30秒,锁是自动删除吗,是不会的,Redisson一旦加锁成功就会启动一个watch dog【看门狗】,当时间每过期1/3时,就检查一下,如果当前线程还继续持有锁,就会重新刷新到30秒,直到最后的锁释放。
可以看到,通过watch dog机制确保不会在业务程序结束之前存储到Redis的锁过期。
可以在Redisson的Config对象中设置锁的默认存时间:config.setLockWatchdogTimeout(10 * 1000);
需要注意的是,如果在获取锁时指定了leaseTime
参数,看门狗程序是不会生效的,如下:
上述的配置,锁的有效期时间为10秒,10秒后锁会自动释放,不会续期。
:::info
面试官问: