支付系統設計實現(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("支付碼非法,請刷新二維碼");
    }
相關文章
相關標籤/搜索