支付系统设计实现(3)——扫码支付

扫码支付场景

用户在食堂,超市进行限订金额的交易时,能够经过出示支付二维码,商家使用扫码器进行扫码,全部收款操做由商家端完成,进行免密码的支付。其中用户的手机能够是离线的,可是扫码器必须是联网的。 服务器

7624f044b492c81527e470beda9f7f3.png

业务分析

根据上述支付宝给出的流程图,咱们能够将步骤梳理以下:异步

  1. 用户打开APP,展现二维码。此时不论手机是否联网,APP都能生成二维码,说明二维码是在APP生成的
  2. 收银员生成订单,代表订单的信息,金额是在收银端完成的,也就是说此时的订单只是产生了,可是并无和具体的APP帐户相关联。
  3. 收银员经过扫码器扫描用户展现的二维码,进行交易。这一步商户后台会进行订单的支付操做。也就是说扫码器得到的二维码是包含了用户信息而且能够解析得到的
  4. 收银端实时返回交易结果。
  5. APP异步收到交易结果通知。

根据以上步骤,咱们绘制以下时序图:
24701.png加密

接口整理

/**
     * 获取被动扫码支付token
     * @param accountId 帐户id
     * @return 仅以一次有效的token
     */
    String getPayScanPassivityToken(String accountId);
    
    /**
     * 被动扫码消费
     * @param payCode 支付码
     * @param consumeAmount 支付金额
     * @param businessId 业务id(食堂订单id,洗衣订单id)
     * @param businessIdType 业务类型
     * @param tradeRemark 交易备注
     * @return 支付帐户id
     */
    String consumeByScanPassivity(String payCode, int consumeAmount, String businessId, WalletConsumeType businessIdType,String tradeRemark) throws InvalidOperationException;
    
     /**
     * 根据支付payCode查询支付状态
     * @param payCode 支付码
     * @return 支付状态
     */
    WalletOrderStatus getWalletOrderStatusByToken(String payCode) throws InvalidOperationException;

这里须要注意的是,APP每次得到的支付令牌token,二维码是在token的基础上进行加密绘制的,二维码本质上是一个支付码payCode,扫码器每次得到是payCode,用于交易的是payCode,须要插叙支付状态的也是payCode。若是用户一开始就没有token,那么APP是没法进行二维码绘制的。咱们使用payCode而不是token进行支付的目的就是离线能够屡次支付spa

payCode的校验

APP生成payCode的时间与服务器校验payCode的时间是有偏差的,咱们限定的是先后15分钟有效。若是payCode在60 * 15 个备选数据中有一个符合,咱们都认为校验成功。下面是关键代码:code

/**
     * 检验payCode
     * @param payCode 支付码
     * @return 返回帐户id
     */
    private String checkPayCode(String payCode) throws InvalidOperationException {
        log.info("===" + payCode);
        final int payCodeLessLength = 24;
        //容许15分钟有效
        final long payCodeTimeStep = 60 * 15;
        final String payCodePrivateKey = "5d4*********************************";

        if(StringUtils.isEmpty(payCode) || payCode.length() <= payCodeLessLength){
            throw new InvalidOperationException("支付码格式异常,请从新扫码");
        }
        String payTokenStr = payCode.substring(payCode.length() - payCodeLessLength);
        Optional<WalletPayTokenEntity> payTokenOptional = walletPayTokenJpaRepository.findByToken(payTokenStr);
        if(!payTokenOptional.isPresent()){
            log.error("支付码不存在:" + payCode);
            throw new InvalidOperationException("支付码不存在,请刷新二维码");
        }
        WalletPayTokenEntity payToken = payTokenOptional.get();
        if(!EntityStatusEnum.VALID.getValue().equals(payToken.getEntityStatus())){
            throw new InvalidOperationException("支付码已经消费,请刷新二维码");
        }
        long timestamp = LocalDateTimeUtils.getSecondsByTime(LocalDateTime.now());
        String accountId = payToken.getAccountId();
        for (long i = timestamp - payCodeTimeStep; i <= timestamp + payCodeTimeStep; i++) {
            String raw = accountId + i + payCodePrivateKey;
            String signCode = DigestUtils.sha1Hex(raw.getBytes()) + payTokenStr;
            if(payCode.equals(signCode)){
                payToken.consume(payCode);
                walletPayTokenJpaRepository.save(payToken);
                return accountId;
            }
        }
        log.error("支付码非法:" + payCode);
        throw new InvalidOperationException("支付码非法,请刷新二维码");
    }
相关文章
相关标签/搜索