1.交易的本质是什么?

买东西付款,卖东西收款。
交易需要理解的几个维度:
1)谁收款?
2)谁付款?
3)付多少钱?
4)付款/收款是否成功?
5)通过什么渠道收款/收款?

支付有两种:消费者直接支付给运营平台;消费者直接支付给商家。

2.说出交易业务调用时序流程?

支付时序图.png
1)客户端收银员发起收银请求;
2)在订单服务中根据订单生成交易单信息;
3)在订单服务发起RPC(远程调用)支付服务的支付请求;
4)在支付服务中给支付中的订单加锁,选择交易适配路由(选择支付宝、微信、京东钱包等第三方支付系统),这里选择了设计模式中的适配器模式:根据交易渠道适配不同的三方支付系统的实现,注意:交易渠道必须由前端选择传递;
5)校验交易单的完整性,交易单应包含如下内容:订单编号、支付金额、交易渠道、商家信息、交易类型:付款/退款(FK/TK)、安全性校验:大额交易/常用支付地变更(IP)/频繁交易/密码变更/支付设备变更/收款方类型…..
6)保证支付和三方系统的幂等性,支付服务的幂等性校验(如,订单编号orderNo),三方系统幂等性的保证(交易单编号-雪花算法)
7)生成交易单号
8)统一下单,通过http请求调用三方系统,注意:自己封装请求报文,必须【加密】处理报文;
9)三方系统解密成功后,返回一个二维码链接给支付服务,这个二维码链接是第三方加密后的结果;
10)支付服务将二维码的链接解密后返回给客户端
11)买家通过扫码付款,将money直接转给第三方
12)通过第三方异步推送支付结果给支付服务;或者在支付服务设置定时任务,主动轮询三方系统,查询支付结果;
13)如果查询的结果是支付成功,修改订单和交易单状态;如果查询的结果是支付失败,状态修改为未支付;

3.交易统一接口入参和出参分别是什么? 接口的作用是什么?

交易入参(PayChannelVo交易渠道):
交易入参.png
核心参数:
channel_name(渠道名称) channel_label(通道唯一标记) domian(域名)
app_id(商户id) public_key(公钥) merchant_private_key(商户私钥)
other_config(其他配置,这里是JSON格式) encrypt_key(AES混淆密钥)

交易出参(TradingVo交易单):
交易单的参数.png
核心参数:
productOrderNo(业务系统订单号) tradingOrderNo(交易系统订单号[对于第三方来说: 商户订单])
tradingChannel(支付渠道[支付宝,微信,现金,免单挂账]) tradingType(交易类型[付款,退款,面单,挂账])
tradingState(交易单状态[DFK待付款,FKZ付款中,QXDD取消订单,YJS已结算,GZ挂账])
第三方交易返回的参数:
resultCode(第三方交易返回编码即最终确认交易结果) resultMsg(第三方交易返回提示信息即最终确认交易信息) resultJson(第三方交易返回信息json[分析交易最终信息])
支付服务返回给客户端的信息:
placeOrderCode(统一下单返回编码) placeOrderMsg(统一下单返回信息)
placeOrderJson(统一下单返回信息Json[用于生产二维码、Android、ios唤醒支付等])
交易UML图.png
XXXPsyFace:提供支付微服务的dubbo接口,供其他微服务调用

  1. 比如:
  2. NativePayFace:用户选择在线支付方式,扫描商户生成的二维码支付;
  3. CashPayFace:现金支付方式,商户收完钱,直接操作即可;
  4. CreditPayFace:信用支付方式,有免单或挂账;

XXXPayAdaoter:支付适配器接口,解决应用系统中对接的实现不兼容,让这些不兼容的实现一起组合工作, 使用接口规范,其实现类采用分布式锁的方式给订单加锁,防止支付订单出现并发问题

  1. NativePayAdapter:Native在线支付方式的适配器、调用RegisterBeanHandlerIOC中获得对应实现
  2. CashPayAdapter:现金支付方式的适配器
  3. CreditPayAdapter:信用支付方式的适配器

XXXPayHandler接口说明: 具体处理方式接口定义,幂等性及前置参数的校验

  1. NativePayHandlerNative在线支付相关处理:支付宝支付,微信支付等相关操作(统一下单、交易查询、统一退款、退款查询等)
  2. CashPayHandler:现金支付相关处理:(支付、退款)
  3. CreditPayHandler:信用支付相关处理:免单、挂账


4.校验交易单完整性有哪些?

订单、订单号、商户ID、交易金额、支付渠道这些如果为空,说明交易单是不完整的。

5.为什么需要加锁?

是为了支付服务在统一收单交易预创建时出现并发问题,导致同一时间出现多张交易单,单个用户支付多次支付。
使用redis来实现分布式锁的,采用的是Hash结构,key是业务前缀 + 系统的订单号。
调用getFairLock()方法给订单加公平锁。公平锁:保证了当多个线程同时请求加锁,优先分配给先发出请求的线程。

6.什么是交易适配路由? 有什么作用? 怎么实现的?

在线支付方式(比如:支付宝,微信支付,JD钱包等第三方系统)的接口互相间是不兼容的,为了在项目始终使它们相互兼容能一起工作,所以使用了设计模式中的适配器模式。还有让项目的扩展性更高。
实现:
1)定义一个适配器的接口:NativePayAdapter接口

  1. // 查看二维码信息
  2. String queryQrCodeUrl(OrderVo orderVo);
  3. // 统一收单线下交易预创建
  4. TradingVo createDownLineTrading(TradingVo tradingVo);
  5. // 统一收单线下交易查询
  6. radingVo queryDownLineTrading(TradingVo tradingVo);
  7. // 统一收单交易退款接口
  8. TradingVo refundDownLineTrading(TradingVo tradingVo);
  9. // 统一收单交易退款查询接口
  10. void queryRefundDownLineTrading(RefundRecordVo refundRecordVo);

2)定义NativePayAdapter接口的实现类NativePayAdapterImpl实现具体的接口方法,并且将该实现类的实例交由spring管理

  1. private static Map<String,String> nativePayHandlers =new HashMap<>();
  2. static {
  3. nativePayHandlers.put(TradingConstant.TRADING_CHANNEL_ALI_PAY, "aliNativePayHandler");
  4. nativePayHandlers.put(TradingConstant.TRADING_CHANNEL_WECHAT_PAY,"wechatNativePayHandler");
  5. }

在适配器实现类有一个核心步骤:把NativePayHandler接口在适配器的实现类里,定义为Map集合的属性,Map的key是在线支付方式的支付渠道(由前端传过来的),value是NativePayHandler实现类在Spring IOC容器中bean的名称,内部类加上static要让支付接口的实现类,随着适配器实现类的加载而加载。通过前端传递的 支付渠道到map里面获取到对应的 value(字符串),然后通过spring 上下文对象 getBean就可以获取到对应实现类。
适配器的流程.png

7.什么是交易处理接口? 作用是什么? 怎么实现的?

项目的交易处理接口是NativePayHandler,是定义在线支付方式的接口(具体的在线支付方式都得实现此接口),在改接口定义的如下的方法:

方法的功能查询支付的二维码、统一下单、交易查询、统一退款、退款查询的功能;
实现:
1)定义一个交易处理的接口:NativePayHandler接口:接口方法如下

  1. // 查看二维码信息
  2. String queryQrCodeUrl(OrderVo orderVo);
  3. // 统一收单线下交易预创建
  4. TradingVo createDownLineTrading(TradingVo tradingVo);
  5. // 统一收单线下交易查询
  6. radingVo queryDownLineTrading(TradingVo tradingVo);
  7. // 统一收单交易退款接口
  8. TradingVo refundDownLineTrading(TradingVo tradingVo);
  9. // 统一收单交易退款查询接口
  10. void queryRefundDownLineTrading(RefundRecordVo refundRecordVo);

2)native支付方式的实现类实现此接口,比如有AliNativePayHandler、WechatNativeHandler等:
在具体的实现类里,有交易单参数校验,对交易的订单进行幂等性处理(判断订单状态是否为已结算免单、付款中、取消订单或者其他状态一律抛异常,如果是首次支付则生成交易单号【使用雪花算法生成】);

8.为什么还要有交易单?

1、交易单是专门用于对接交易支付系统
2、订单和具体业务相关,交易单和业务无关, 解耦
3、交易系统是一个通用支付服务

9.支付查询结果如何保证一定能获取到?

支付结果查询在项目中采用了两种方式来实现:
1)第三方系统异步推送支付结果给餐掌柜的支付服务
*如果出现网络抖动或延迟咋办?这个问题是由第三方来解决的,就我了解到的第三方设计是:如果我们服务收不到回调通知请求,第三方会按照某个频率一直发送给我们的服务,直到我们获取到数据(这里会有一个幂等性操作)。
2)我们服务主动轮询(项目使用)
项目中的支付服务主动定时任务轮询查询订单的支付结果
这两种方式搭配使用的话,可以确保我们的系统一定能获取到用户的支付结果。

10.实现支付配置功能如何设计?

思考:
1、微信支付(appid,mchid、证书)
2、支付宝支付(appid、公钥、私钥、entry_key…)
3、对接jd(a,b,c,d)
问题:

  • 数据很重要保存到数据库表中,表结构该怎么设计呢?
  • 先去找共用的字段,设置到表结构
  • 找不同的字段,不同的部分可以存到一个字段varchar(1000):JSON格式
    {“a”,”1”,”b”,”2”} ——- 表中表

    11.商家入驻到平台之后需要做什么事情?

    首先在微信支付平台和支付宝支付平台注册账号,得到各种支付配置(appId,私钥、秘钥、公钥或证书),然后再商家平台的支付渠道中把这些支付配置传到后端,然后再保存到的数据库的tab_pay_channel表中。

    12.支付宝退款状态查询时间是怎么建议的?

    退款状态。枚举值:REFUND_SUCCESS 退款处理成功;未返回该字段表示退款请求未收到或者退款失败;
    注:如果退款查询发起时间早于退款时间,或者间隔退款发起时间太短,可能出现退款查询时还没处理成功功,后面又处理成功的情况,第三方系统限流的情形,建议商户在退款发起后间隔10秒以上再发起退款查询请求

    13.支付的两个问题:重复支付和重新支付怎么解决?

    通过幂等性校验可以解决这个问题。在项目是通过判断订单状态是否为已结算免单、付款中、取消订单或者其他状态一律抛异常,如果是首次支付则生成交易单号【使用雪花算法生成】

    14.交易单号是什么时候生成的?

    是在交易单的幂等性校验之后,如果该笔订单为首次支付,则生成交易单号。

    15.你们是如何解决支付隔离的问题的?

    有两种实现一是选择商家ID;二是选择门店ID。项目选择的是商家ID,
    将配置文件初始化,项目一加载就将商家的支付配置从数据库中缓存到redis中,在redis的结构是String结构,大key是业务前缀 + 商户ID,value是PayChannelVo对象。只要商家每次发起支付请求,在后端都会通过商家ID去redis中查询对应的商家支付配置。
    支付渠道配置放在了 各个商家的管理后台,保存在【数据库】中
    特征:
    1、访问很频繁 — 热点
    2、不经常改变 — 更新频率
    3、数据很重要,应该持久化到数据库 — 数据重要
    具体实现:

    1. @PostConstruct
    2. public void InitAliPayConfig() {
    3. //1、查询所有商户支付配置列表
    4. //根据支付通道再数据库查询所有商户的支付配置
    5. List<PayChannel> payChannelList = payChannelService.findPayChannelList(TradingConstant.TRADING_CHANNEL_ALI_PAY);
    6. List<PayChannelVo> payChannelVoList = BeanConv.toBeanList(payChannelList, PayChannelVo.class);
    7. if (!EmptyUtil.isNullOrEmpty(payChannelVoList)) {
    8. //遍历支付配置
    9. payChannelVoList.forEach(n->{
    10. //otherConfig字段是已JSON格式存储的,所以再这里需要进行转换为OtherConfigVo类的对象
    11. List<OtherConfigVo> otherConfigVos = JSONArray.parseArray(n.getOtherConfig(), OtherConfigVo.class);
    12. n.setOtherConfigs(otherConfigVos);
    13. //在redis中获得通用对象桶,key是业务前缀 + 商户ID(ps:redis的通用对象桶是用来存储任意类型对象的)
    14. RBucket<PayChannelVo> alipayChannel = redissonClient.getBucket("pay:alipay:channel:" + n.getEnterpriseId());
    15. //把支付通道的对象保存到redis中
    16. alipayChannel.set(n);
    17. });
    18. }
    19. }

    16.你们项目中xxl-job是如何使用的?

    在支付结果和退款结果查询时有使用到定时任务(xxl-job)

  • 在任务调度中心中配置执行器(名称)、配置任务

  • 名称:支付状态同步执行器 AppName:model-trading-job-listen
  • 名称:基础服务同步执行器 AppName:model-basic-job-listen
  • 路由策略是怎么配置,你是怎么思考?

路由策略选择的是第一个,项目中使用定时任务是用来定时从第三方系统查询支付结果或者退款结果
即使执行器采用的集群部署, 我们只需要知道第一台执行器查询的结果即可。

  • 阻塞处理策略,怎么思考?

单机串行:调度请求进入单机执行器后,调度请求进入FIFO队列并以串行方式运行;在项目中支付本身就被加了分布式锁,即同一时刻只能有一笔订单进入支付状态,只有当该笔订单支付成功后下笔订单进入支付状态,所以使用单机串行也是贴合业务的。

  • 定时任务怎么配置

采用每10秒(0/10 **?),主动从第三方系统查询支付结果或退款结果。**

  • JobHandler和项目代码@XXljob(“”) ```java @XxlJob(value = “nativePayHandlerJob”) @GlobalTransactional public ReturnT execute(String param) { //查询所有支付中的订单 List tradingVoList = tradingFace.findTradingByTradingState(TradingConstant.FKZ); for (TradingVo tradingVo : tradingVoList) {
    1. this.synchTradingState(tradingVo);
    } ReturnT.SUCCESS.setMsg(“执行-支付同步-成功”); return ReturnT.SUCCESS;

} ```

  • 失败重试次数=0

不需要重试,10秒后又会再一次执行定时任务,相当于会将所有的未成功的请求再重新执行。

思考:
假设 10w个商家, 每个商家有2个门店, 每个门店 20个桌台,每个桌台至少可以每天使用4次,每天订单成交量 70%,
问每天会产生多少条订单? 每月多少条订单? 一年多少条订单?
假设每条订单数据大小是 2kb, 问一年产生多大数据量,单位:GB? 1TB=1024GB 1GB=1024MB
每天会产生1120万条订单,每月3.36亿条订单,每年40.32亿条订单;一年产生的数据量:76904.296875GB