019:基于模板方法模式重构异步回调
- 1 基于策略+模板方法实现异步回调重构
- 2 异步回调通知实现的原理
- 3 支付宝官方demo异步回调代码的实现
- 4 聚合支付项目如何采用模板+策略重构
- 5 基于模板模式重构聚合支付平台
- 6 异步回调重构验证签名代码
- 7 基于策略模式id找到模板类
- 8 异步回调更改订单状态的信息
- 9 支付异步回调代码继续重构
1 基于策略+模板方法实现异步回调重构
今日课程任务
- 第三方支付异步回调实现的原理
- 基于模板+策略模式实现异步回调
- 基于ThreadLocal传递参数信息内容
- 采用多线程/MQ异步写入支付的日志信息参数
- 如何防止支付金额不一致的问题
- 如何防止支付回调幂等性问题
- 支付服务存在那些分布式事务难题
2 异步回调通知实现的原理
同步回调:以浏览器重定向方式跳转
异步回调:第三方支付以post请求格式通知给商户端 修改订单状态
支付宝官网文档:https://opendocs.alipay.com/open/270/105899
调用流程如下:
- 商户系统调用 alipay.trade.page.pay(统一收单下单并支付页面接口)向支付宝发起支付请求,支付宝对商户请求参数进行校验,而后重新定向至用户登录页面。
- 用户确认支付后,支付宝通过 get 请求 returnUrl(商户入参传入),返回同步返回参数。
- 交易成功后,支付宝通过 post 请求 notifyUrl(商户入参传入),返回异步通知参数。
- 若由于网络等原因,导致商户系统没有收到异步通知,商户可自行调用alipay.trade.query(统一收单线下交易查询)接口查询交易以及支付信息(商户也可以直接调用该查询接口,不需要依赖异步通知)。
先触发异步回调,再触发同步回调。
注意:
- 由于同步返回的不可靠性,支付结果必须以异步通知或查询接口返回为准,不能依赖同步跳转。
- 商户系统接收到异步通知以后,必须通过验签(验证通知中的sign参数)来确保支付通知是由支付宝发送的。详细验签规则请参见 异步通知验签。
- 接收到异步通知并验签通过后,请务必核对通知中的app_id、out_trade_no、total_amount等参数值是否与请求中的一致,并根据trade_status进行后续业务处理。
- 在支付宝端,partnerId与out_trade_no唯一对应一笔单据,商户端保证不同次支付out_trade_no不可重复;若重复,支付宝会关联到原单据,基本信息一致的情况下会以原单据为准进行支付。
3 支付宝官方demo异步回调代码的实现
异步回调中的商户订单号码与支付宝交易号码有哪些区别?
商户订单号码—支付的id
支付宝交易号码—支付宝流水号码 补偿
如果商户端没有及时返回success给支付宝的情况下,支付宝可能会触发重试策略,重试过程中可能会发生幂等性问题。
注意:同步回调以浏览器重定向形式返回,商户端只是解析报文验证签名成功,将支付结果告诉给用户,但是不能修改订单状态。
4 聚合支付项目如何采用模板+策略重构
如何对聚合支付回调进行设计重构?
策略+模板方法组合
策略模式:解决多重if判断的问题
模板方法模式:定义共同骨架(代码)统一都放入在父类,不同的代码让子类实现。
异步回调代码
1 记录日志
2 验证签名(不同)
3 修改订单状态为已支付
4 调用积分接口增加积分
5 基于模板模式重构聚合支付平台
模板方法骨架类
@Slf4j
public abstract class AbstractPayCallbackTemplate {
@Autowired
private PayThreadInfoHolder payThreadInfoHolder;
public String asyncCallBack() {
// 1. 验证签名
boolean isSign = verifySignature();
// 2. 记录日志 注意问题:一定要异步处理(为了快速响应给支付宝避免重试造成的幂等性问题)
addPayLog();
if (!isSign) {
// 银联支付的时候 必须返回ok;支付宝必须返回success
return resultFail();
}
// 更改订单状态
return asyncService();
}
/**
* 3.更改订单状态已经支付
* 4.调用积分服务接口增加积分
* @return
*/
protected abstract String asyncService();
/**
* 交给子类实现
*
* @return
*/
protected abstract String resultFail();
/**
* 返回成功
*
* @return
*/
protected abstract String resultSuccess();
/**
* 定义成抽象方法让子类实现
* 不管验证签名成功还是失败都会将参数放入到ThreadLocal中
* @return
*/
protected abstract boolean verifySignature();
/**
* 将该方法单独放入在一个类中调用,@Async不经过代理类不会生效
*/
public void addPayLog() {
// 从ThreadLocal中获取
Map<String, String> requestParams = payThreadInfoHolder.getRequestParams();
log.info(">>>记录支付的日志requestParams:<<<" + requestParams);
}
}
使用ThreadLocal存放参数
@Component
public class PayThreadInfoHolder {
private ThreadLocal<Map<String, String>> payThreadLocal = new ThreadLocal();
public void setRequestParams(Map<String, String> requestParams) {
payThreadLocal.set(requestParams);
}
public Map<String, String> getRequestParams() {
return payThreadLocal.get();
}
}
6 异步回调重构验证签名代码
验证签名
@Component
@Slf4j
public class AliPayCallbackTemplate extends AbstractPayCallbackTemplate {
@Autowired
private PayThreadInfoHolder payThreadInfoHolder;
@Override
protected String resultFail() {
return "fail";
}
@Override
protected String resultSuccess() {
return "success";
}
@Override
protected boolean verifySignature(PaymentChannelEntity pce) {
// 根据当前线程获取Request
HttpServletRequest request = ServletUtils.getCurrentRequest();
HttpServletResponse response = ServletUtils.getCurrentResponse();
//获取支付宝POST过来反馈信息
Map<String, String> params = new HashMap<String, String>();
Map<String, String[]> requestParams = request.getParameterMap();
ServletOutputStream out = null;
try {
out = response.getOutputStream();
for (Iterator<String> iter = requestParams.keySet().iterator(); iter.hasNext(); ) {
String name = (String) iter.next();
String[] values = (String[]) requestParams.get(name);
String valueStr = "";
for (int i = 0; i < values.length; i++) {
valueStr = (i == values.length - 1) ? valueStr + values[i]
: valueStr + values[i] + ",";
}
//乱码解决,这段代码在出现乱码时使用
valueStr = new String(valueStr.getBytes("ISO-8859-1"), "utf-8");
params.put(name, valueStr);
// 防止验证签名失败
params.remove("channelId");
}
boolean signVerified = AlipaySignature.rsaCheckV1(
params, pce.getPublicKey(), AlipayConfig.CHARSET, AlipayConfig.SIGN_TYPE); //调用SDK验证签名
if (signVerified) {
//验证成功
//商户订单号
String outTradeNo = new String(request.getParameter("out_trade_no").getBytes("ISO-8859-1"), "UTF-8");
//支付宝交易号
String tradeNo = new String(request.getParameter("trade_no").getBytes("ISO-8859-1"), "UTF-8");
//交易状态
String tradeStatus = new String(request.getParameter("trade_status").getBytes("ISO-8859-1"), "UTF-8");
if (tradeStatus.equals("TRADE_FINISHED")) {
} else if (tradeStatus.equals("TRADE_SUCCESS")) {
}
out.println("success");
return true;
} else {
//验证失败
return false;
}
} catch (Exception e) {
log.error("verifySignature()>> errorMsg:" + e.getMessage());
return false;
} finally {
try {
if (out != null) {
out.close();
}
} catch (Exception e) {
}
payThreadInfoHolder.setRequestParams(params);
}
}
@Override
protected String asyncService() {
return null;
}
}
7 基于策略模式id找到模板类
数据库payment_channel表新增字段callback_bean_id,aliPayCallbackTemplate对应支付回调模板子类AliPayCallbackTemplate。
异步回调方法
public interface PayCallbackService {
/**
* 异步回调
*
* @return
*/
@RequestMapping("/asynCallback")
Object asynCallback(String channelId);
}
@RestController
public class PayCallbackServiceImpl extends BaseApiService implements PayCallbackService {
@Autowired
private PaymentChannelMapper paymentChannelMapper;
@Override
public Object asynCallback(String channelId) {
if (StringUtils.isEmpty(channelId)) {
return setResultError("channelId不能为空");
}
// 1.根据渠道id查询渠道信息
PaymentChannelEntity pce = paymentChannelMapper.selectBychannelId(channelId);
if (pce == null) {
return setResultError("该渠道已关闭或不存在,请联系管理员");
}
//2.获取模板的baenid
String callbackBeanId = pce.getCallbackBeanId();
if (StringUtils.isEmpty(callbackBeanId)) {
return setResultError("没有配置callbackBeanId");
}
//3.直接从Spring容器上下文获取
AbstractPayCallbackTemplate payCallbackTemplate =
SpringContextUtils.getBean(callbackBeanId, AbstractPayCallbackTemplate.class);
String result = payCallbackTemplate.asyncCallBack(pce);
return result;
}
}
8 异步回调更改订单状态的信息
阿里支付回调模板实现类
@Component
@Slf4j
public class AliPayCallbackTemplate extends AbstractPayCallbackTemplate {
@Autowired
private PayThreadInfoHolder payThreadInfoHolder;
@Autowired
private PaymentTransactionMapper paymentTransactionMapper;
@Override
protected String resultFail() {
return "fail";
}
@Override
protected String resultSuccess() {
return "success";
}
@Override
protected boolean verifySignature(PaymentChannelEntity pce) {
// 根据当前线程获取Request
HttpServletRequest request = ServletUtils.getCurrentRequest();
HttpServletResponse response = ServletUtils.getCurrentResponse();
//获取支付宝POST过来反馈信息
Map<String, String> params = new HashMap<String, String>();
Map<String, String[]> requestParams = request.getParameterMap();
ServletOutputStream out = null;
try {
out = response.getOutputStream();
for (Iterator<String> iter = requestParams.keySet().iterator(); iter.hasNext(); ) {
String name = (String) iter.next();
String[] values = (String[]) requestParams.get(name);
String valueStr = "";
for (int i = 0; i < values.length; i++) {
valueStr = (i == values.length - 1) ? valueStr + values[i]
: valueStr + values[i] + ",";
}
//乱码解决,这段代码在出现乱码时使用
valueStr = new String(valueStr.getBytes("ISO-8859-1"), "utf-8");
params.put(name, valueStr);
// 防止验证签名失败
params.remove("channelId");
}
boolean signVerified = AlipaySignature.rsaCheckV1(
params, pce.getPublicKey(), AlipayConfig.CHARSET, AlipayConfig.SIGN_TYPE); //调用SDK验证签名
if (signVerified) {
out.println("success");
return true;
} else {
//验证失败
return false;
}
} catch (Exception e) {
log.error("verifySignature()>> errorMsg:" + e.getMessage());
return false;
} finally {
try {
if (out != null) {
out.close();
}
} catch (Exception e) {
}
payThreadInfoHolder.setRequestParams(params);
}
}
@Override
protected String asyncService() {
// 1. 根据支付订单号码查询数据库 ,是否已经支付过
Map<String, String> requestParams = currentRequestParams();
// 支付订单号码
String outTradeNo = requestParams.get("out_trade_no");
PaymentTransactionEntity pte = paymentTransactionMapper.selectByPaymentId(outTradeNo);
// 以下代码可以重构进入父类里面
if ("1".equals(pte.getPaymentStatus())) {
// 返回成功告诉给支付不要再继续重试
return resultSuccess();
}
// 2.判断支付的金额 是否一致
Long payAmount = pte.getPayAmount();
String totalAmount = requestParams.get("total_amount");
Long aliPayAmount = yuanToFen(totalAmount);
if (!payAmount.equals(aliPayAmount)) {
// 返回success,后台将该订单状态修改为异常订单状态,然后分析异常原因用定时任务处理
return resultSuccess();
}
// 3. 在修改为已经支付
int result = paymentTransactionMapper.updatePaymentStatus("1", outTradeNo, "ali_pay");
if (result < 0) {
// 返回失败告诉支付宝重试
return resultFail();
}
// 调用积分服务接口增加积分 存在分布式事务的问题
return resultSuccess();
}
private static Long yuanToFen(String amount) {
NumberFormat format = NumberFormat.getInstance();
try {
Number number = format.parse(amount);
double temp = number.doubleValue() * 100.0;
format.setGroupingUsed(false);
// 设置返回数的小数部分所允许的最大位数
format.setMaximumFractionDigits(0);
amount = format.format(temp);
return Long.parseLong(amount);
} catch (ParseException e) {
return null;
}
}
}
测试效果:
9 支付异步回调代码继续重构
重构asyncService()方法 -修改订单状态是通用的逻辑,可以放在父类中实现
@Slf4j
public abstract class AbstractPayCallbackTemplate {
@Autowired
private PayThreadInfoHolder payThreadInfoHolder;
@Autowired
private PaymentTransactionMapper paymentTransactionMapper;
public String asyncCallBack(PaymentChannelEntity pce) {
// 1. 验证签名
boolean isSign = verifySignature(pce);
// 2. 记录日志 注意问题:一定要异步处理(为了快速响应给支付宝避免重试造成的幂等性问题)
addPayLog();
if (!isSign) {
// 银联支付的时候 必须返回ok;支付宝必须返回success
return resultFail();
}
// 更改订单状态 获取订单号码,支付金额
String orderNo = getPayOrderNo();
Long totalAmount = getTotalAmount();
return asyncService(orderNo, totalAmount);
}
/**
* 3.更改订单状态已经支付
* 4.调用积分服务接口增加积分
*
* @return
*/
private String asyncService(String outTradeNo, Long totalAmount) {
// 支付订单号码
PaymentTransactionEntity pte = paymentTransactionMapper.selectByPaymentId(outTradeNo);
// 以下代码可以重构进入父类里面
if (PayConstant.PAY_PAID_STATUS.equals(pte.getPaymentStatus())) {
// 返回成功告诉给支付不要再继续重试
return resultSuccess();
}
// 2.判断支付的金额 是否一致
Long payAmount = pte.getPayAmount();
if (!payAmount.equals(totalAmount)) {
// 返回success,后台将该订单状态修改为异常订单状态,然后分析异常原因用定时任务处理
return resultSuccess();
}
// 3. 在修改为已经支付
int result = paymentTransactionMapper.updatePaymentStatus(PayConstant.PAY_PAID_STATUS + "", outTradeNo, "ali_pay");
if (result < PayConstant.DB_FAIL) {
// 返回失败告诉支付宝重试
return resultFail();
}
// 调用积分服务接口增加积分 存在分布式事务的问题
return resultSuccess();
}
/**
* 获取真实的支付订单号码
*
* @return
*/
protected abstract String getPayOrderNo();
/**
* 获取真实的支付金额
*
* @return
*/
protected abstract Long getTotalAmount();
/**
* 交给子类实现
*
* @return
*/
protected abstract String resultFail();
/**
* 返回成功
*
* @return
*/
protected abstract String resultSuccess();
/**
* 定义成抽象方法让子类实现
* 不管验证签名成功还是失败都会将参数放入到ThreadLocal中
*
* @return
*/
protected abstract boolean verifySignature(PaymentChannelEntity pce);
/**
* 将该方法单独放入在一个类中调用,@Async不经过代理类不会生效
*/
public void addPayLog() {
// 从ThreadLocal中获取
Map<String, String> requestParams = currentRequestParams();
log.info(">>>记录支付的日志requestParams:<<<" + requestParams);
}
protected Map<String, String> currentRequestParams() {
return payThreadInfoHolder.getRequestParams();
}
}
@Component
@Slf4j
public class AliPayCallbackTemplate extends AbstractPayCallbackTemplate {
@Autowired
private PayThreadInfoHolder payThreadInfoHolder;
@Override
protected String getPayOrderNo() {
// 1. 根据支付订单号码查询数据库 ,是否已经支付过
Map<String, String> requestParams = currentRequestParams();
// 支付订单号码
String outTradeNo = requestParams.get("out_trade_no");
return outTradeNo;
}
@Override
protected Long getTotalAmount() {
// 1. 根据支付订单号码查询数据库 ,是否已经支付过
Map<String, String> requestParams = currentRequestParams();
String totalAmount = requestParams.get("total_amount");
Long aliPayAmount = yuanToFen(totalAmount);
return aliPayAmount;
}
@Override
protected String resultFail() {
return "fail";
}
@Override
protected String resultSuccess() {
return "success";
}
@Override
protected boolean verifySignature(PaymentChannelEntity pce) {
// 根据当前线程获取Request
HttpServletRequest request = ServletUtils.getCurrentRequest();
HttpServletResponse response = ServletUtils.getCurrentResponse();
//获取支付宝POST过来反馈信息
Map<String, String> params = new HashMap<String, String>();
Map<String, String[]> requestParams = request.getParameterMap();
ServletOutputStream out = null;
try {
out = response.getOutputStream();
for (Iterator<String> iter = requestParams.keySet().iterator(); iter.hasNext(); ) {
String name = (String) iter.next();
String[] values = (String[]) requestParams.get(name);
String valueStr = "";
for (int i = 0; i < values.length; i++) {
valueStr = (i == values.length - 1) ? valueStr + values[i]
: valueStr + values[i] + ",";
}
//乱码解决,这段代码在出现乱码时使用
valueStr = new String(valueStr.getBytes("ISO-8859-1"), "utf-8");
params.put(name, valueStr);
// 防止验证签名失败
params.remove("channelId");
}
boolean signVerified = AlipaySignature.rsaCheckV1(
params, pce.getPublicKey(), AlipayConfig.CHARSET, AlipayConfig.SIGN_TYPE); //调用SDK验证签名
if (signVerified) {
out.println("success");
return true;
} else {
//验证失败
return false;
}
} catch (Exception e) {
log.error("verifySignature()>> errorMsg:" + e.getMessage());
return false;
} finally {
try {
if (out != null) {
out.close();
}
} catch (Exception e) {
}
payThreadInfoHolder.setRequestParams(params);
}
}
private static Long yuanToFen(String amount) {
NumberFormat format = NumberFormat.getInstance();
try {
Number number = format.parse(amount);
double temp = number.doubleValue() * 100.0;
format.setGroupingUsed(false);
// 设置返回数的小数部分所允许的最大位数
format.setMaximumFractionDigits(0);
amount = format.format(temp);
return Long.parseLong(amount);
} catch (ParseException e) {
return null;
}
}
}