扫码支付场景
用户在食堂,超市进行限订金额的交易时,可以通过出示支付二维码,商家使用扫码器进行扫码,所有收款操作由商家端完成,进行免密码的支付。其中用户的手机可以是离线的,但是扫码器必须是联网的。
业务分析
根据上述支付宝给出的流程图,我们可以将步骤梳理如下:
- 用户打开APP,展示二维码。此时不论手机是否联网,APP都能生成二维码,说明二维码是在APP生成的。
- 收银员生成订单,表明订单的信息,金额是在收银端完成的,也就是说此时的订单只是产生了,但是并没有和具体的APP账户相关联。
- 收银员通过扫码器扫描用户展示的二维码,进行交易。这一步商户后台会进行订单的支付操作。也就是说扫码器获得的二维码是包含了用户信息并且可以解析获得的。
- 收银端实时返回交易结果。
- APP异步收到交易结果通知。
根据以上步骤,我们绘制如下时序图:
接口整理
/** * 获取被动扫码支付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进行支付的目的就是离线可以多次支付。
payCode的校验
APP生成payCode的时间与服务器校验payCode的时间是有误差的,我们限定的是前后15分钟有效。如果payCode在60 * 15 个备选数据中有一个符合,我们都认为校验成功。下面是关键代码:
/** * 检验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("支付码非法,请刷新二维码"); }