1 快速入门

首先看在途支付整体设计部分文档,包括项目分层,数据库结构的设计,系统关键词解释。本文档,以下模块按照不同业务操作来进行划分,例如收银台支付,直接支付扣款,退款等业务操作。首先根据时序图来展示支付系统和各个系统组件的交互,会说明涉及到了那些接口,然后对重要逻辑进行解释,给出一些常见问题的参考排除思路。

2 进入收银台

2.1 时序图

  • 步骤2,注意考虑两个网关,一个是payment-web 的 PayAction.java类,一个是tem-h5 的PayController.java类,负责对外提供http服务,这里请求的两个web 端地址分别是

    1
    2
    pc: payment.z-trip.cn/pay-channel.html?orderIds=订单号;
    h5: ms.z-trip.cn/status.json?orderNos=订单号;
  • 步骤3,获取订单支付详情的接口,这个接口的返回值对排除支付问题有帮助

    1
    2
    // PaymentService.java 接口
    PaymentInfoDto getPaymentInfoDetail(Long userId, String orderNos, Long customServiceId);
  • 步骤4,调用各个业务线实现的获取订单的支付信息接口

    1
    2
    // OrderProcessService.java 接口
    List<OrderPaymentInfo> getPaymentInfo(String orderNos);
  • 步骤6,在上一步获取到了各个订单的支付信息后,在步骤6里组装收银台需要的支付信息,发起企业支付/个人支付的基本信息。例如:订单的总金额,订单是否支持企业支付,本次支付是否需要审批等。

  • 步骤8,根据返回的支付大对象,在pc端 和 h5端渲染收银台,例如公司支付是否显示,个人支付是否显示,订单的摘要信息等。

2.2 重要逻辑说明

上述时序图中步骤6构建的支付对象为整个构建支付对象过程的核心,代码实现在PaymentInfoBuilder.java 类内。这个类的构造方法有三个参数:

1
2
3
4
5
public PaymentInfoBuilder(List<OrderPaymentInfo> paymentInfos, Long userId, Long customServiceId) {
this.paymentInfos = paymentInfos;
this.userId = userId;
this.customServiceId = customServiceId;
}
  • 第一个参数为 从各个业务线接口获取的订单支付的基本信息,OrderPaymentInfo 对象如果需要修改字段,需要协调各个业务线进行配合;
  • 第二个参数为 收银台传过来的当前登录人的Id,这个字段主要是用来做一个校验,确保当前操作人企业和订单的企业是一致的,当前操作人是订单预订人;
  • 第三个参数为 收银台传过来的当前登录的客服的Id,这个参数用来判断是不是客服代客下单操作。

需要构建的支付对象(PaymentInfoDto)有什么属性,订单的支付信息(OrderPaymentInfo)有什么元素,请参阅代码。这里把整个支付大对象的构建抽象为分为以下几个构建点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public abstract class Builder {

/**
* 1. 构建支付订单基本信息
*/
abstract void buildOrderInfo();

/**
* 2. 构建订单总金额,企业支付最大金额,差标金额等
*/
abstract void buildTotalAmount();

/**
* 3. 构建账户信息
*/
abstract void buildAccountInfo();

/**
* 4. 构建验证点
*/
abstract void buildVerifyPoint();

/**
* 5. 构建二次支付的信息
*/
abstract void buildSecondaryPay();

/**
* 6. 构建企业支付才需要的验证点
*/
abstract void buildBpPayPoint();

/**
* 7. 构建支付批次号
*/
abstract void buildPaymentBatch();

/**
* 8. 加密支付信息,去支付的时候需要用
*/
abstract void buildEncryptedInfo();

/**
* 获取支付信息
* @return
*/
public abstract PaymentInfoDto getPaymentInfoDto();
}
  • 模块1 构建支付订单的基本信息,例如用户Id, 企业id, 成本中心id, 支付关联的订单列表(orderInfos) 等基本信息,这个划分也不是很严格,第一步就放一些没有逻辑的属性设置;

  • 模块2 构建订单总金额,企业支付最大金额,差标金额等这里涉及到了金额计算,注意金额计算的精度问题,计算的总金额请保留小数点后两位;

  • 模块3 计算个人账户的信息,比如个人账户的可用余额信息;

  • 模块4 构建验证点,这个复杂点,按照代码的执行顺序,从上至下分别对各个校验点校验。注意如果添加校验逻辑请更新注释,而且也需要注意添加逻辑的代码的顺序,以下为简要逻辑说明,具体以代码实现为准:

    • 订单能否支付
      • 操作人是否合法,例如支付不是自己预订的订单;
      • 订单的支付金额小于0,收银台支持支付0元的订单,不支持支付0元以下的订单;
      • 因公因私订单不能合并支付,因公和因私的订单不能同时进入收银台支付;
    • 设置订单的过期时间
      • 收银台需要显示订单的过期时间倒计时
    • 是否显示个人支付
      • 第一次支付的个人支付金额为0,二次支付不显示个人支付
    • 是否能用个人余额支付
      • 可用金额大于0,可以使用个人余额,可用金额小于等于0,不可以使用个人余额
    • 是否显示公司支付
      • 管控规则不允许使用企业支付,不显示
      • 违规信息不允许使用企业支付,不显示
      • 差旅类型不唯一不允许使用企业支付,不显示
      • 不能显示企业支付,有出租车订单
      • 不能显示企业支付,因私订单
      • 不能显示企业支付,成本中心不唯一
      • 不能显示企业支付,找不到企业账户
    • 是否是因私自付
      • 有因私订单就是因私自付
    • 是否能用公司支付

      • 企业账户余额不足
    • 是否能使用公司支付全额支付
      • 如果选择了超标自付,就不能使用公司公司支付全额支付
    • 是否能使用超标自付

      • 企业开启了超标自付权限
      • 管控规则中这个业务线是否能用超标自付
      • 需要有订单的金额大于差标金额
  • 模块5 二次支付相关

  • 二次支付订单不能和非二次支付订单一起支付

  • 模块6 构建企业支付相关校验点

    • 是否超出差旅标准
    • 是否有非员工
    • 是否有沿用授权订单
    • 企业支付支付是否需要授权 companyPayAuthorize
      • 0:公司支付不需要授权 1: 公司支付需要授权 2: 违规原因只有超标支付,选择超标自付不需要授权
      • 二次支付不需要授权
      • 有沿用授权订单不需要授权
      • 考虑目前酒店业务涉及超标自付的授权
    • 是否需要确认差旅事由
      • 这个校验在前台需要和授权一起使用,需要授权的支付,需要确认差旅事由
    • 是否需要确认密码

      • 这个校验在前台需要和授权一起使用,不需要授权的支付,需要输入登录密码
  • 模块7 构建支付批次号,防止一个支付对象,在网关层发起了两次支付

  • 模块8 加密支付的基本信息

    • 加密的这部分支付基本信息,需要传给调用支付的接口时,传给支付接口。

2.3 问题排查参考思路

2.3.1 无法使用公司支付
  1. 首先看,是不是企业账户不可显示的原因导致,上文列出了企业账户不可用的系列原因点,结合阿里云日志判断。
  2. 然后,可能会提示余额不足,这时候就需要企业自己充值。
2.3.2 无法使用【公司支付 - 全额支付】

可以使用公司支付,但是不能选择【公司支付 - 全额支付】的原因只有一个,就是用户选择了超标自付,这个时候最稳妥的办法,就是看订单系统的,是否选择超标自付的字段。

2.3.3 无法使用【公司支付 - 超标自付】

无法使用【公司支付- 超标自付】,就按照前面提到的三个点,依次排查:1. 企业是否开启了超标自付,2. 业务线是否开启了超标自付,3. 有订单的金额大于差标金额,才可以使用超标自付。

2.3.4 收银台支付审批和预期不正确
  1. 第一步,看业务系统数据库,查看授权字段的值设置是否正确,因为需要授权是根据业务系统告诉支付系统的,酒店业务线目前有 这个字段有 0,1,2 这个值,其他业务线有 0,1 两个值;
  2. 第二步,查看阿里云日志,看下是否用了沿用授权,使用了沿用授权,可以免审;
  3. 第三步,看下是不是订单二次企业支付,二次支付不需要授权。
2.3.5 个人账户有钱 - 但是不显示

这个时候,你需要去排查一下,是不是取个人账户可用金额的时候报错了。

2.4 一些注意点

  1. 金额字段在支付系统存储用的Long类型,保存的单位为分。对外部系统,包括业务系统,都是用的 BigDecimal, 单位是元。在进行计算转换的时候,注意精读和单位;
  2. 企业账户/个人账户,都会对账户记录进行加密,如果要手动修改数据库账户的记录,请更新加密的字段,以免获取企业账户/个人账户的时候,会报错。

3 客户选择支付通道

3.1 时序图

​ 一般性来讲,用户选择支付通道这个逻辑,应该是前台处理的逻辑。但是考虑到这里逻辑有点小复杂,为了统一pc/h5处理,目前这个逻辑放在了支付服务端。

  • 步骤1 ,收银台显示了用户可选择的所有支付通道;
  • 步骤4,根据一些逻辑处理,返回需要勾选的支付通道和金额。

3.2 重要逻辑说明

这里的所有逻辑都在PayChannelUtils.listPayChannel(ChooseChannelInfo channelInfo) 方法内,具体的规则,请参阅支付的产品文档。这里只做一个实现的说明。

  1. 点击了那个,那个就是选中的,其他之前选中的可能会取消,也可能会保持;
  2. 参考通道之间的互斥。企业全额支付和企业超标自付只能选中一个,支付宝支付和微信支付只能选中一个等;
  3. 需要根据是否能使用各种支付通道,来进行逻辑校验;
  4. 返回各个通道支付的金额,和剩余还应该支付的金额。
3.2.1 收银台选择通道逻辑

pc 和 h5 选择收银通道略有不同,pc 端,每个选项后面都有支付金额。由于h5没有,所以h5特殊一点,具体可以结合pc / h5 收银台 与上面逻辑 自己尝试一下。

3.3 问题排查参考思路

3.3.1 按钮点击无响应

大概率是支付服务挂了,或者前台js 报错了。

3.4 一些注意点

pc 端和 h5 端,勾选支付通道逻辑不一致,注意分别修改维护。

4 收银台发起支付

4.1 时序图

4.1.1 至少包括一种现付
image-20190525153800599
image-20190525153800599

​ 上面的流程是包含现付的支付流程,如果本次支付通道包含现付,则先在支付系统里面生成支付计划,然后先进行现付的流程,待客户完成现付,在现付的异步回调里面,再走其他企业支付/个人支付的账户扣款操作流程。

  • 步骤4中,返回的state为100时,说明走现付的流程,这时候去进行现付页面跳转;
  • 注意步骤6,7,8,9,通过调用 pingxx 封装的js 类库,进行第三方支付页面跳转;
  • 步骤11 中,会返回同步的支付成功结果;
  • 步骤15 中,pingxx 会回调 payment-web 的 支付通知接口,这里是异步的支付结果回调,在这个回调里面,从缓存里面拿一下,还没有完成的支付信息(如果有),进行还未完成的支付通道扣款操作;
  • 步骤18 中,为mq 异步回调业务系统,支付成功。
4.1.2 不包含现付的支付方式

​ 上述时序图是不包括现付的支付通道组合。可以为企业支付/个人账户支付。

  • 步骤5 中,如果企业支付需要审批,这时候需要调用工作流,走审批流;
  • 步骤6 中,如果企业支付需要审批,这时候支付结果编码为审批中,如果不需要审批,这时候支付结果为支付完成。
4.1.3 企业审批通过/驳回

​ 上述时序图为 支付系统响应企业审批结果。

  • 步骤1,用户审批的方式有多种,例如,pc/h5 界面审批,短信/微信审批等;
  • 步骤2,支付系统接收mq 发送的审批结果消息;
  • 步骤3, 如果是审批通过,则进行企业账户冻结转支付的操作。如果是审批驳回,则需要进行 企业账户解冻的操作,还需要把个人支付的金额进行原理返回操作;
  • 步骤4,发送支付授权完成事件,消息中心响应,会发送授权完成的消息;
  • 步骤5,发送 审批通过/驳回/撤销/取消的消息。

4.2 重要逻辑说明

这里对收银台支付的MixedPaymentService.pay(MixedPaymentInfo mixedPaymentInfo) 方法的处理进行解析。

4.2.1 根据action层参数组装 PaymentInfo

这一步会根据你在 web层选择了那几个支付通道,从而计算出每个通道支付的金额。会根据是不是客服登录且选择忽略审批直接支付而改变是否需要授权,会根据是不是选择了忽略审批,从而直接支付。这一层的逻辑,其实也可以放到action层。为了pc 和 h5 的统一,现在放在了这里。

4.2.2 根据 PaymentInfo 进行支付
目前的类结构图

  • OperationExecutor 操作执行器工具类,用来运行操作器的相关方法。

  • Operation 操作器,定义一个操作器的基本 init(), execute(), rollback() 方法。

  • PayOperation 支付操作器,继承自Operation,添加支付操作的相关行为,prePay(), doPay(), postPay()。
  • AbstractPay,抽象支付操作类,实现PayOperation,实现基本的操作器的相关行为,例如:支付初始化,初始化日志锁,异常账户回滚实现等。
  • MixedPayOperation 真正支付操作实现类,继承 AbstractPay,实现 PayOperation的 prePay ,doPay,postPay 方法。

4.3 问题排查参考思路

4.3.1 业务系统未收到支付回调
  1. 第一步,根据订单号,去支付系统数据库查一下支付计划,看一下支付计划的状态,支付行项目的状态,确定一下订单是否完成了支付,还是订单处于待审批的状态;
  2. 第二步,查看支付系统日志,看下是否打印了发送支付结果的mq消息,复制一下这个消息,确定一下支付的状态;
  3. 第三步,看了以上两步,如果支付系统没有问题,那么基本可以确定是业务系统自己处理的问题了,也有额外情况,例如 mq 发送失败等。这时候去notify 数据库查一下mq 的编码,再去阿里云看一下mq消息轨迹,确定一下mq 消息有没有被消费过。
4.3.2 订单双重支付
  1. 首先第一步分析一下两次支付的过程,注意每次操作的时间间隔;
  2. 从浏览器端分析,会不会是一个支付对象,被用户多次点击支付了,这种情况已经在每个支付对象加了uuid来避免出现了,单还是要注意一下;
  3. 从 pingxx 多次回调分析,会不会是pingxx 多次回调,导致进行了二次支付;
  4. 从业务系统端思考,由于支付系统支持二次支付,会不会是第一次支付的时候,业务系统没有进行正确的业务处理,而且让用户进行了第二次支付。
4.3.3 现付回调未收到
  1. 第一步,检查各个环境的回调url 是否能够ping 通;
  2. 第二步,去pingxx 检查回调url 的配置;
  3. 第三步,去pingxx 查询回调的发送;这一步一般性没有问题哦。

4.4 一些注意点

注意项目的分层和代码结构的抽象。

5 调用退款接口

5.1 时序图

  1. 步骤一,用户申请退款是业务系统先处理;
  2. 步骤二,业务系统计算每个订单可退金额,尤其是改签单,需要计算出原单可退金额,和改签单可退金额;
  3. 步骤三,业务系统调用支付的退款接口,发起退款;
  4. 步骤四,企业支付和个人支付,要按照比例分别退款;
  5. 步骤五,如果现付退款,还需要进行现付退款。

5.2 重要逻辑说明

5.2.1 现付资金的流向图

  • 现付付款在整个支付系统的流向图
  • 业务系统出票失败或者拒单退款后,客户现付付款的钱,转入个人余额,记录一笔个人账户行项目
  • 客户使用个人账户付款,扣去个人余额,记录在途物资
  • 个人账户退款,增加个人账户余额,更新账户行项目
  • 定时任务,把个人账户行项目对应的金额,原路返回
5.2.2 计算订单的可退金额
  1. 业务系统退款时,会把这个退款单关联的所有正向订单都传过来,例如改签单的退款,会把改签单和改签单的原单都传过来;
  2. 求这个退款订单的剩余可退金额,累加每个正向订单的支付金额 减去 每个正向订单的退款金额,就是剩余可退金额;
  3. 如果正向订单有多种支付方式,求出每个通道需要退款的金额;

5.3 问题排查参考思路

5.3.1 退款金额超出可退金额
  1. 这个一般情况是业务系统反馈过来,先查看业务系统的请求日志;
  2. 通过数据库计算出,这个订单的可退金额;
  3. 比较下可退金额和业务系统需要退的金额;
5.3.2 客户反馈未收到资金
  1. 根据支付计划号,看下pingxx,看下是否有退款记录;
  2. 查看退款计划号,查看管理的退款任务,查看退款的定时任务;
  3. 结合日志和数据库进行判断。

6. 支付待完善点

6.1 分层的完善

项目结构分层还不是很完善,包结构比较乱。

6.2 支付拦截器的应用

把所有对外的接口的返回值修改为 ResponseDto,然后在事物拦截器的外层加上 PaymentInterceptor 支付拦截器,这个拦截器的功能在于,事物提交或回滚后的额外处理。目前这个拦截器已经写了,可以找个迭代运用上去。