淘东电商项目(61) -聚合支付(基于模板方法设计模式管理支付回调)

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 淘东电商项目(61) -聚合支付(基于模板方法设计模式管理支付回调)

引言

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

阅读本文前,有兴趣的同学可以参考我之前写的聚合支付的文章:

目前「淘东电商项目」的聚合支付模块,已经完成了银联支付以及支付宝的集成。接下来本文主要讲解基于"模板设计模式"的方式来管理银联支付和支付宝支付结果回调接口。

本文目录结构:

l____引言

l____ 1. 什么是模板设计模式?

l____ 2. 项目核心代码

l____ 3. 测试

1. 什么是模板设计模式?

关于模板设计模式,之前我有写过博客记录过,童鞋们有兴趣的可以参阅下:

抽象模板(Abstract Template)角色有如下责任:

  • 定义了一个或多个抽象操作,以便让子类实现。这些抽象操作叫做基本操作,它们是一个顶级逻辑的组成步骤。
  • 定义并实现了一个模板方法。这个模板方法一般是一个具体方法,它给出了一个顶级逻辑的骨架,而逻辑的组成步骤在相应的抽象操作中,推迟到子类实现。顶级逻辑也有可能调用一些具体方法。

具体模板(Concrete Template)角色又如下责任:

  • 实现父类所定义的一个或多个抽象方法,它们是一个顶级逻辑的组成步骤。
  • 每一个抽象模板角色都可以有任意多个具体模板角色与之对应,而每一个具体模板角色都可以给出这些抽象方法(也就是顶级逻辑的组成步骤)的不同实现,从而使得顶级逻辑的实现各不相同。

好了,下面开始讲解核心的代码。

2. 项目核心代码

①定义抽象操作,让子类去实现(AbstractPayCallbackTemplate):

/**
 * description: 使用模版方法重构异步回调代码
 * create by: YangLinWei
 * create time: 2020/5/18 9:34 上午
 */
@Slf4j
public abstract class AbstractPayCallbackTemplate {
    /**
     * 获取所有请求的参数,封装成Map集合 并且验证是否被篡改
     *
     * @param req
     * @param resp
     * @return
     */
    public abstract Map<String, String> verifySignature(HttpServletRequest req, HttpServletResponse resp);
    /**
     * description: 同步回调执行业务逻辑
     * param:
     *
     * @return
     */
    public abstract String syncService(HttpServletRequest req, HttpServletResponse resp) throws UnsupportedEncodingException;
    /**
     * 异步回调执行业务逻辑
     *
     * @param verifySignature
     */
    public abstract String asyncService(Map<String, String> verifySignature);
    public abstract String failResult();
    public abstract String successResult();
    /**
     * *1. 将报文数据存放到es <br>
     * 1. 验证报文参数<br>
     * 2. 将日志根据支付id存放到数据库中<br>
     * 3. 执行的异步回调业务逻辑<br>
     */
    public String asyncCallBack(HttpServletRequest req, HttpServletResponse resp) {
        // 1. 验证报文参数 相同点 获取所有的请求参数封装成为map集合 并且进行参数验证
        Map<String, String> verifySignature = verifySignature(req, resp);
        // 2.将日志根据支付id存放到数据库中
        String paymentId = verifySignature.get("paymentId");
        if (StringUtils.isEmpty(paymentId)) {
            return failResult();
        }
        // 3.采用异步形式写入日志到数据库中
        payLog(paymentId, verifySignature);
        String result = verifySignature.get(PayConstant.RESULT_NAME);
        // 4.201报文验证签名失败
        if (result.equals(PayConstant.RESULT_PAYCODE_201)) {
            return failResult();
        }
        // 5.执行的异步回调业务逻辑
        return asyncService(verifySignature);
    }
    /**
     * 采用多线程技术或者MQ形式进行存放到数据库中
     *
     * @param paymentId
     * @param verifySignature
     */
    private void payLog(String paymentId, Map<String, String> verifySignature) {
        log.info(">>paymentId:{paymentId},verifySignature:{}", verifySignature);
    }
}

②子类实现(UnionPayCallbackTemplate):

/**
 * description: 银联支付回调模版实现
 * create by: YangLinWei
 * create time: 2020/5/18 9:32 上午
 */
@Component
public class UnionPayCallbackTemplate extends AbstractPayCallbackTemplate {
  @Autowired
  private PaymentTransactionMapper paymentTransactionMapper;
  @Override
  public Map<String, String> verifySignature(HttpServletRequest req, HttpServletResponse resp) {
    LogUtil.writeLog("BackRcvResponse接收后台通知开始");
    String encoding = req.getParameter(SDKConstants.param_encoding);
    // 获取银联通知服务器发送的后台通知参数
    Map<String, String> reqParam = getAllRequestParam(req);
    LogUtil.printRequestLog(reqParam);
    // 重要!验证签名前不要修改reqParam中的键值对的内容,否则会验签不过
    if (!AcpService.validate(reqParam, encoding)) {
      LogUtil.writeLog("验证签名结果[失败].");
      reqParam.put(PayConstant.RESULT_NAME, PayConstant.RESULT_PAYCODE_201);
    } else {
      LogUtil.writeLog("验证签名结果[成功].");
      // 【注:为了安全验签成功才应该写商户的成功处理逻辑】交易成功,更新商户订单状态
      String orderId = reqParam.get("orderId"); // 获取后台通知的数据,其他字段也可用类似方式获取
      reqParam.put("paymentId", orderId);
      reqParam.put(PayConstant.RESULT_NAME, PayConstant.RESULT_PAYCODE_200);
    }
    LogUtil.writeLog("BackRcvResponse接收后台通知结束");
    return reqParam;
  }
  @Override
  public String syncService(HttpServletRequest req, HttpServletResponse resp) throws UnsupportedEncodingException {
    LogUtil.writeLog("FrontRcvResponse前台接收报文返回开始");
    String encoding = req.getParameter(SDKConstants.param_encoding);
    LogUtil.writeLog("返回报文中encoding=[" + encoding + "]");
    String pageResult = "";
    Map<String, String> respParam = getAllRequestParam(req);
    // 打印请求报文
    LogUtil.printRequestLog(respParam);
    Map<String, String> valideData = null;
    StringBuffer page = new StringBuffer();
    if (null != respParam && !respParam.isEmpty()) {
      Iterator<Map.Entry<String, String>> it = respParam.entrySet()
          .iterator();
      valideData = new HashMap<String, String>(respParam.size());
      while (it.hasNext()) {
        Map.Entry<String, String> e = it.next();
        String key = (String) e.getKey();
        String value = (String) e.getValue();
        value = new String(value.getBytes(encoding), encoding);
        page.append("<tr><td width=\"30%\" align=\"right\">" + key
            + "(" + key + ")</td><td>" + value + "</td></tr>");
        valideData.put(key, value);
      }
    }
    if (!AcpService.validate(valideData, encoding)) {
      page.append("<tr><td width=\"30%\" align=\"right\">验证签名结果</td><td>失败</td></tr>");
      LogUtil.writeLog("验证签名结果[失败].");
    } else {
      page.append("<tr><td width=\"30%\" align=\"right\">验证签名结果</td><td>成功</td></tr>");
      LogUtil.writeLog("验证签名结果[成功].");
      System.out.println(valideData.get("orderId")); //其他字段也可用类似方式获取
      String respCode = valideData.get("respCode");
      //判断respCode=00、A6后,对涉及资金类的交易,请再发起查询接口查询,确定交易成功后更新数据库。
    }
    return page.toString();
  }
  // 异步回调中网络尝试延迟,导致异步回调重复执行 可能存在幂等性问题
  //
  @Override
  public String asyncService(Map<String, String> verifySignature) {
    String orderId = verifySignature.get("orderId"); // 获取后台通知的数据,其他字段也可用类似方式获取
    String respCode = verifySignature.get("respCode");
    // 判断respCode=00、A6后,对涉及资金类的交易,请再发起查询接口查询,确定交易成功后更新数据库。
    System.out.println("orderId:" + orderId + ",respCode:" + respCode);
    // 1.判断respCode是否为已经支付成功断respCode=00、A6后,
    if (!(respCode.equals("00") || respCode.equals("A6"))) {
      return failResult();
    }
    // 根据日志 手动补偿 使用支付id调用第三方支付接口查询
    PaymentTransactionEntity paymentTransaction = paymentTransactionMapper.selectByPaymentId(orderId);
    if (paymentTransaction.getPaymentStatus().equals(PayConstant.PAY_STATUS_SUCCESS)) {
      // 网络重试中,之前已经支付过
      return successResult();
    }
    // 2.将状态改为已经支付成功
    paymentTransactionMapper.updatePaymentStatus(PayConstant.PAY_STATUS_SUCCESS + "", orderId);
    // 3.调用积分服务接口增加积分(处理幂等性问题)
    return successResult();
  }
  @Override
  public String failResult() {
    return PayConstant.YINLIAN_RESULT_FAIL;
  }
  @Override
  public String successResult() {
    return PayConstant.YINLIAN_RESULT_SUCCESS;
  }
  /**
   * 获取请求参数中所有的信息 当商户上送frontUrl或backUrl地址中带有参数信息的时候,
   * 这种方式会将url地址中的参数读到map中,会导多出来这些信息从而致验签失败,
   * 这个时候可以自行修改过滤掉url中的参数或者使用getAllRequestParamStream方法。
   * 
   * @param request
   * @return
   */
  public static Map<String, String> getAllRequestParam(final HttpServletRequest request) {
    Map<String, String> res = new HashMap<String, String>();
    Enumeration<?> temp = request.getParameterNames();
    if (null != temp) {
      while (temp.hasMoreElements()) {
        String en = (String) temp.nextElement();
        String value = request.getParameter(en);
        res.put(en, value);
        // 在报文上送时,如果字段的值为空,则不上送<下面的处理为在获取所有参数数据时,判断若值为空,则删除这个字段>
        if (res.get(en) == null || "".equals(res.get(en))) {
          // System.out.println("======为空的字段名===="+en);
          res.remove(en);
        }
      }
    }
    return res;
  }
  /**
   * 获取请求参数中所有的信息。
   * 非struts可以改用此方法获取,好处是可以过滤掉request.getParameter方法过滤不掉的url中的参数。
   * struts可能对某些content-type会提前读取参数导致从inputstream读不到信息,所以可能用不了这个方法。
   * 理论应该可以调整struts配置使不影响,但请自己去研究。
   * 调用本方法之前不能调用req.getParameter("key");这种方法,否则会导致request取不到输入流。
   * 
   * @param request
   * @return
   */
  public static Map<String, String> getAllRequestParamStream(final HttpServletRequest request) {
    Map<String, String> res = new HashMap<String, String>();
    try {
      String notifyStr = new String(IOUtils.toByteArray(request.getInputStream()), UnionPayBase.encoding);
      LogUtil.writeLog("收到通知报文:" + notifyStr);
      String[] kvs = notifyStr.split("&");
      for (String kv : kvs) {
        String[] tmp = kv.split("=");
        if (tmp.length >= 2) {
          String key = tmp[0];
          String value = URLDecoder.decode(tmp[1], UnionPayBase.encoding);
          res.put(key, value);
        }
      }
    } catch (UnsupportedEncodingException e) {
      LogUtil.writeLog("getAllRequestParamStream.UnsupportedEncodingException error: " + e.getClass() + ":"
          + e.getMessage());
    } catch (IOException e) {
      LogUtil.writeLog("getAllRequestParamStream.IOException error: " + e.getClass() + ":" + e.getMessage());
    }
    return res;
  }
  /**
   * 回调机制 必须遵循规范 重试机制都是采用间隔新 错开的话 必须
   */
}

③定义模板工厂(TemplateFactory):

/**
 * description: 获取具体实现的模版工厂方案
 * create by: YangLinWei
 * create time: 2020/5/18 9:30 上午
 */
public class TemplateFactory {
  public static AbstractPayCallbackTemplate getPayCallbackTemplate(String beanId) {
    return (AbstractPayCallbackTemplate) SpringContextUtil.getBean(beanId);
  }
}

④定义回调接口:

@RestController
public class PayAsynCallbackService {
  private static final String UNIONPAYCALLBACK_TEMPLATE = "unionPayCallbackTemplate";
  /**
   * 银联同步回调接口执行代码
   *
   * @param req
   * @param resp
   * @return
   */
  @RequestMapping("/unionPaySynCallback")
  public String unionPaySynCallback(HttpServletRequest req, HttpServletResponse resp) throws UnsupportedEncodingException {
    AbstractPayCallbackTemplate abstractPayCallbackTemplate = TemplateFactory
        .getPayCallbackTemplate(UNIONPAYCALLBACK_TEMPLATE);
    return abstractPayCallbackTemplate.syncService(req, resp);
  }
  /**
   * 银联异步回调接口执行代码
   * 
   * @param req
   * @param resp
   * @return
   */
  @RequestMapping("/unionPayAsynCallback")
  public String unionPayAsynCallback(HttpServletRequest req, HttpServletResponse resp) {
    AbstractPayCallbackTemplate abstractPayCallbackTemplate = TemplateFactory
        .getPayCallbackTemplate(UNIONPAYCALLBACK_TEMPLATE);
    return abstractPayCallbackTemplate.asyncCallBack(req, resp);
  }
}

3. 测试

首先启动项目(Eureka注册中心、单点服务中心、会员服务、支付服务、支付门户):

①配置银联支付的同步和异步回调地址,其SQL语句如下:

INSERT INTO `taodong-pay`.`payment_channel`(`ID`, `CHANNEL_NAME`, `CHANNEL_ID`, `MERCHANT_ID`, `SYNC_URL`, `ASYN_URL`, `PUBLIC_KEY`, `PRIVATE_KEY`, `CHANNEL_STATE`, `REVISION`, `CREATED_BY`, `CREATED_TIME`, `UPDATED_BY`, `UPDATED_TIME`, `CLASS_ADDRES`) VALUES (1, '银联支付', 'yinlian_pay', '777290058110048', 'http://localhost:8600/unionPaySynCallback', 'http://7z4nwp.natappfree.cc/unionPayAsynCallback', NULL, NULL, 0, NULL, NULL, NULL, NULL, NULL, 'com.ylw.service.pay.strategy.impl.UnionPayStrategy');

②浏览器模拟提交订单:请求地址http://localhost:8600/cratePayToken?payAmount=999&orderId=20200513141452&userId=27&productName=玉米香肠

数据库订单状态为未支付:

③模拟使用银联支付,浏览器请求:http://localhost:8079/pay?payToken=pay_f78c020ba2ec4c278e2e47a72e5c7c57

④.点击银联支付,按照提示一步一步支付至完成:

可以看到订单状态为已支付:

点击返回商户按钮,可以看到同步回调也是成功的:

支付宝回调与银联支付也是一样的原理,有有兴趣的同学可以下载代码相信看看。本文完!

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
6月前
|
设计模式 Java API
重构旧代码的秘诀:用设计模式 - 适配器模式(Adapter)给Java项目带来新生
【4月更文挑战第7天】适配器模式是解决接口不兼容问题的结构型设计模式,通过引入适配器类实现目标接口并持有不兼容类引用,实现旧代码与新接口的协作。适用于处理兼容性问题、整合遗留代码和集成第三方库。应用时,识别不兼容接口,创建适配器类转换方法调用,然后替换原有引用。注意保持适配器简单、使用组合和考虑扩展性。过度使用可能导致系统复杂和维护成本增加,应谨慎使用。
100 4
|
6月前
|
设计模式 算法 Java
模板方法--设计模式
模板方法--设计模式
49 0
|
6月前
|
设计模式 算法 Java
Java一分钟之-设计模式:策略模式与模板方法
【5月更文挑战第17天】本文介绍了策略模式和模板方法模式,两种行为设计模式用于处理算法变化和代码复用。策略模式封装不同算法,允许客户独立于具体策略进行选择,但需注意选择复杂度和过度设计。模板方法模式定义算法骨架,延迟部分步骤给子类实现,但过度抽象或滥用继承可能导致问题。代码示例展示了两种模式的应用。根据场景选择合适模式,以保持代码清晰和可维护。
112 1
|
2月前
|
设计模式 数据库连接 PHP
PHP中的设计模式:如何提高代码的可维护性与扩展性在软件开发领域,PHP 是一种广泛使用的服务器端脚本语言。随着项目规模的扩大和复杂性的增加,保持代码的可维护性和可扩展性变得越来越重要。本文将探讨 PHP 中的设计模式,并通过实例展示如何应用这些模式来提高代码质量。
设计模式是经过验证的解决软件设计问题的方法。它们不是具体的代码,而是一种编码和设计经验的总结。在PHP开发中,合理地使用设计模式可以显著提高代码的可维护性、复用性和扩展性。本文将介绍几种常见的设计模式,包括单例模式、工厂模式和观察者模式,并通过具体的例子展示如何在PHP项目中应用这些模式。
|
6月前
|
设计模式 Java API
【设计模式】JAVA Design Patterns——Aggregator Microservices(聚合器微服务模式)
【设计模式】JAVA Design Patterns——Aggregator Microservices(聚合器微服务模式)
|
3月前
|
设计模式 存储 前端开发
揭秘.NET架构设计模式:如何构建坚不可摧的系统?掌握这些,让你的项目无懈可击!
【8月更文挑战第28天】在软件开发中,设计模式是解决常见问题的经典方案,助力构建可维护、可扩展的系统。本文探讨了.NET中三种关键架构设计模式:MVC、依赖注入与仓储模式,并提供了示例代码。MVC通过模型、视图和控制器分离关注点;依赖注入则通过外部管理组件依赖提升复用性和可测性;仓储模式则统一数据访问接口,分离数据逻辑与业务逻辑。掌握这些模式有助于开发者优化系统架构,提升软件质量。
54 5
|
3月前
|
设计模式 JavaScript 前端开发
小白请看 JS大项目宝典:设计模式 教你如何追到心仪的女神
小白请看 JS大项目宝典:设计模式 教你如何追到心仪的女神
|
4月前
|
设计模式 Java 数据库连接
Java中的设计模式在实际项目中的应用
Java中的设计模式在实际项目中的应用
|
5月前
|
设计模式 Java
设计模式在Java项目中的实际应用
设计模式在Java项目中的实际应用
|
4月前
|
设计模式 Java
设计模式在Java项目中的实际应用
设计模式在Java项目中的实际应用