2023-04-16
场景与实战
0

目录

支付
支付体系整体架构
核心业务流程分析
支付流程设计
产品架构图
极简支付系统架构图
完整支付系统架构图及各子系统简介
核心系统依赖图
拓展知识
技术风险与资损防控
识别风险来自哪里
应对风险
内部系统实时与离线对账
技术方案实现
数据库设计
支付状态机设计
支付网关设计
支付渠道适配
支付幂等性保障
金额处理规范
业务ID生成规则
日志规范
异步通知处理
分布式事务解决方案
支付对账系统
支付渠道降级策略
数据一致性保障
监控与告警
支付安全
支付安全核心关注点
极简支付安全
总结

支付业务是电商系统中技术复杂度最高、稳定性要求最严的模块之一。支付的成功与否,不仅影响用户购物体验,更直接关系到平台的资金安全和收入稳定性。支付系统需要与订单系统、账户体系、风控系统、对账中心等多个模块紧密协作,同时还要对接多个外部支付渠道,技术挑战极大。

支付

在电商业务中,支付模块的稳定性和用户体验至关重要。一个设计良好的支付系统能够有效提升转化率,降低用户流失,而一个有缺陷的支付系统则可能导致资金损失和用户投诉。

支付体系整体架构

支付系统的核心在于稳定性、安全性和扩展性。一个完整的支付体系包含以下核心组件:

image.png

核心业务流程分析

支付流程设计

支付流程从用户提交订单开始,到最终支付成功结束,涉及多个状态转换和系统交互:

image.png

关键节点说明

支付订单生成:基于业务订单创建支付订单,记录支付金额、支付方式等信息

支付渠道调用:根据用户选择的支付方式,调用对应的支付渠道接口

同步结果处理:立即返回支付结果给用户端

异步通知确认:支付渠道通过回调通知最终支付结果,确保数据一致性

产品架构图

所谓产品架构图,简单的理解,就是站在产品角度,提供什么样的服务能力。下面是一个典型的支付系统的产品架构图。这个图画得比较简单,但是已经涵养一个支付系统最核心的产品能力。

image.png

实际实现时差异会很大,尤其是上面的产品或应用层,有很多机构为特殊的行业提供一些特殊的能力,比如携程的支付就会有航空方面的B2B业务。但基础的能力基本也就这些。

上面部分是会员或商户感知的产品能力,包括门户、收银台,收单产品,资金产品等。下面部分是支付系统最核心的服务,用于支撑对外的产品能力。如果多跳几家公司,就会发现基础的部分大家都差不太多。

极简支付系统架构图

这个图很精简,但是基本已经够用,应付本对本交易这种简单的业务是完全没有问题的。

image.png

一些复杂的支付系统可能还有外汇、额度中心、产品中心、卡中心等,甚至一个子系统可能会拆分为多个应用独立部署,比如收单结算就可以拆成收单和结算两个独立的应用。

完整支付系统架构图及各子系统简介

跳过几个支付公司,这些基础的概念在几家公司都差不太多,区别是底层技术实现。比如RPC框架,数据库,业务流程,部署架构等。

image.png

这是一比较完整的系统架构图,属于逻辑划分。在单体应用中,就是一些模块,在分布式应用中,就是一些子域、子应用或子系统。

核心系统依赖图

image.png

开放网关

主要对接商户,比如下单、支付等接口入口。通常要求有比较高的安全性。部分公司可能会把移动端网关、PC门户网关、商户通知等能力集成在开放网关,也可能会单独拆出部署。

收单结算

负责把商户的单收下来,并给商户发起结算。承担的收单产品包括有:线上收单,线下收单,担保交易、即时到账等,每个公司的商业策略不同,开出的收单产品会有差异。

有些公司把结算划到出款中心,对接银企直连的渠道。

资金产品

承担无买卖标的的纯资金转移能力。典型的有:充值、转账、提现、代发。和支付的区分在于支付是有买卖标的,而资金产品没有。也就是在系统中没有买卖记录发生,但在线下可能有。

资金产品一般需要独立的牌照。

收银核心

渲染可用支付方式。包括查询账户是否有余额,查询营销是否有营销券,查询渠道网关是否有可用的外部渠道,最后组合成可用支付方式,供前端渲染。

收银核心就像一个大内总管,收到请求后,找商户平台核实身份,找合约平台核实权限,找会员平台核实用户身份,找收单看一下这笔单是否可以继续支付,找账务中心获取余额信息,营销看看有没有可用的券,找渠道网关看看没有可用的渠道,找额度中心看看是否超限额了,找风控问一下当前支付是否安全,找会员平台校验支付密码 ... ...

支付引擎

负责真正的扣款或转账。有些公司叫支付核心。

如果是余额就调账务扣减余额,如果是红包就调营销做核销,如果是外部银行通道就调渠道网关。

渠道网关

负责去外部渠道扣款。通常还会提供渠道路由、渠道咨询等能力,做得细的公司可能会把渠道核心和报文/文件网关单独拆出来。其中渠道核心就提供渠道路由、渠道咨询、渠道开关等服务,报文/文件网关负责报文转换、签名验签等。

会员平台

管理会员的生命周期,比如注册、注销、登录等。同时还提供核身服务(比如登录密码,支付密码,短信验证码等)、实名认证服务等。

商户平台

管理商户的入驻签约、KYB、交易管理等。

产品中心

管理对外提供的产品能力,比如快捷支付,代扣等。一般大的支付系统才会独立成一个子系统。

账务中心

负责账户开立,记账等。

会计中心

会计科目管理、分录管理、日切管理等。

监管报表有时候也放在这里,有些公司也会独立出去。

很多集团公司往往有一套独立的专业财务系统,这个时候往往需要会计中心做完日切后,要把记账信息合并到集团公司的财务系统中去,简称并账。

对账中心

负责明细对账和资金对账。

营销平台

提供满减、红包等营销工具。

风控平台

针对账户和交易,提供实时、离线风控,控制交易的风险。反洗钱、反欺诈是基本要求。

通常各公司对风控规则看成是机密,研发也可能看不到运营配置的规则。经常看到有网友问:“有xx公司的人在吗?我有xxx场景下的支付总是提示风控不过,是否知道是什么原因,怎么才能通过?”,完全是浪费口舌,谁会对外公布自己的风控规则,让人去钻空子呢?

运营平台

订单管理、渠道管理、产品管理等综合运营工具。

数据平台

主要用于数据汇总和分析。当前各支付公司基本都是分布式部署N多个应用,数据都在散落在各子系统中,需要汇总到数据平台用于经营分析。

卡中心

负责管理用户的绑卡信息。需要经过PCI认证。

额度中心

累计用户、商户的额度,通常有日、月、年,单卡等各种分类。

外汇平台

负责外汇报价和兑换。

流动性与调拨中心

一些跨境支付公司,在多个国家多个银行有头寸,各头寸之间经常需要做流动性管理,提高资金利用率。

毕竟在国外不需要把备付金强制存到央行还不给利息。当资金量大的时候,这笔收益可不少。

差错中心

负责差错处理。比如渠道退款失败(银行账号销户,过了银行的退款有效期等),需要通过其它的方式退给用户。

拒付中心

处理用户的拒付和举证。在跨境支付场景下,信用卡用户联系发卡行说卡被盗刷或商品没有收到,或商品有问题等,拒绝支付给商户。国内基本没有看到拒付场景。

拓展知识

技术风险与资损防控

一般来说,技术风险主要包含稳定性和资损两个方面。其中稳定性风险就是大家经常说的几个9,比如99.999%可用,就是5个9。资损风险就是平台或用户的资金损失。

虽然资损也是技术风险的一种,但是因为对于专业的持牌支付公司来,资损是一种非常严重的事故,容易引发客诉、网络事件、甚至监管介入,所以又较一般的风险更为严重,常常把资损防控单独拿出来说。

技术风险体系过于庞大,这里只谈几点通用知识。

识别风险来自哪里

我们通常先需要知道风险来自哪里,才知道如何去防控。而风险往往来自变化。举几个例子,抛砖引玉:

  • 流量变化:大促场景下,流量会暴增。

  • 代码变化:引入了新的代码。

  • 业务变化:修改了业务流程,或引入了新的业务。

  • 外部变化:外部新的攻击手段。

应对风险

根据变化去应对风险。比如大促引入了流量变化,那就做压测、扩容、限流、降级非核心业务等应对。比如原来只有支付,这些有了用户提现,针对用户提现,内部多个子域可能状态/金额不一致,和银行渠道的状态/金额也可能不一致,那就加入各种对账手段,以及对应的应急预案。

内部系统实时与离线对账

对账是资损防控中最效的手段之一。一般的支付平台会有内部系统之间的两两核对,这种核对主要是信息流层面的核对,主要核对状态、金额的一致性。

image.png

说明:

  1. 可以拆成离线核对和实时核对。
  2. 离线核对一般就是把生产数据库的数据定时清洗到离线库(一般还可以分为天表和小时表)。
  3. 实时核对一般就是监听数据库的binlog,当数据有变动时,延时几秒后请求双方系统的查询接口,查到数据后进行核对。

技术方案实现

数据库设计

支付系统涉及多张核心表,包括支付订单表、支付流水表、退款表等:

支付订单表(payment_order)

sql
CREATE TABLE `payment_order` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `payment_order_no` varchar(32) NOT NULL COMMENT '支付订单号', `business_order_no` varchar(32) NOT NULL COMMENT '业务订单号', `user_id` bigint(20) NOT NULL COMMENT '用户ID', `amount` bigint(20) NOT NULL COMMENT '支付金额(分)', `pay_amount` bigint(20) NOT NULL COMMENT '实付金额(分)', `discount_amount` bigint(20) DEFAULT '0' COMMENT '优惠金额(分)', `payment_channel` varchar(16) NOT NULL COMMENT '支付渠道:ALIPAY、WECHAT、UNIONPAY', `payment_method` varchar(16) NOT NULL COMMENT '支付方式:APP、H5、PC、JSAPI', `payment_status` varchar(16) NOT NULL COMMENT '支付状态', `expire_time` datetime NOT NULL COMMENT '支付过期时间', `pay_time` datetime DEFAULT NULL COMMENT '支付时间', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_payment_order_no` (`payment_order_no`), KEY `idx_business_order_no` (`business_order_no`), KEY `idx_user_id` (`user_id`), KEY `idx_create_time` (`create_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='支付订单表';

支付流水表(payment_flow)

sql
CREATE TABLE `payment_flow` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `payment_order_no` varchar(32) NOT NULL COMMENT '支付订单号', `flow_no` varchar(32) NOT NULL COMMENT '支付流水号', `channel_flow_no` varchar(64) DEFAULT NULL COMMENT '渠道流水号', `request_params` text COMMENT '请求参数', `response_result` text COMMENT '响应结果', `flow_status` varchar(16) NOT NULL COMMENT '流水状态:SUCCESS、FAILED', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_flow_no` (`flow_no`), KEY `idx_payment_order_no` (`payment_order_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='支付流水表';

分库分表

当数据量大的时间,分库分表是再所难免的。

一个经典的面试题是:如果分了100张表,按商户来分表,还是按商户订单号来分表?如果按商户分表怎么解决各表流水数据量平衡问题?如果是按商户订单号来分表,商户想按时间段查询怎么办?

解法有很多种。一种典型的解法,就是线上数据库按商户订单号分表,同时有一个离线库冗余一份按商户号分表的数据,甚至直接使用离线数据平台的能力,把商户的按时间段查询需求从在线库剥离出来。

支付状态机设计

状态机,也称为有限状态机(FSM, Finite State Machine),是一种行为模型,由一组定义良好的状态、状态之间的转换规则和一个初始状态组成。它根据当前的状态和输入的事件,从一个状态转移到另一个状态。

image.png

支付状态机管理支付订单的完整生命周期,确保状态流转的正确性:

java
public enum PaymentStatus { // 待支付 PENDING, // 支付中 PROCESSING, // 支付成功 SUCCESS, // 支付失败 FAILED, // 支付关闭 CLOSED, // 退款中 REFUNDING, // 部分退款 PARTIAL_REFUND, // 全额退款 FULL_REFUND }

状态流转规则:

  • PENDING → PROCESSING:用户发起支付请求

  • PROCESSING → SUCCESS:支付成功

  • PROCESSING → FAILED:支付失败

  • PENDING → CLOSED:订单超时关闭

  • SUCCESS → REFUNDING:发起退款

  • REFUNDING → PARTIAL_REFUND/FULL_REFUND:退款完成

常见代码实现误区

经常看到工作几年的同事实现状态机时,仍然使用if else或switch case来写。这是不对的,会让实现变得复杂,且容易出现问题。

甚至直接在订单的领域模型里面使用String来定义,而不是把状态模式封装单独的类。

还有就是直接调用领域模型更新状态,而不是通过事件来驱动。

支付网关设计

支付网关作为统一入口,负责路由决策、参数组装和协议转换:

java
@Service public class PaymentGatewayService { @Autowired private Map<String, PaymentChannel> paymentChannelMap; /** * 统一支付接口 */ public PaymentResponse pay(PaymentRequest request) { // 1. 参数校验 validatePaymentRequest(request); // 2. 风控检查 riskControlCheck(request); // 3. 选择支付渠道 PaymentChannel channel = selectPaymentChannel(request); // 4. 生成支付订单 PaymentOrder paymentOrder = createPaymentOrder(request); // 5. 调用支付渠道 PaymentResponse response = channel.pay(buildChannelRequest(paymentOrder)); // 6. 记录支付流水 savePaymentFlow(paymentOrder, response); return buildPaymentResponse(response); } /** * 支付渠道路由 */ private PaymentChannel selectPaymentChannel(PaymentRequest request) { String channelKey = request.getPaymentChannel() + "_" + request.getPaymentMethod(); PaymentChannel channel = paymentChannelMap.get(channelKey); if (channel == null) { throw new PaymentException("不支持的支付方式"); } return channel; } }

支付渠道适配

通过策略模式实现多支付渠道的适配,便于扩展和维护:

java
public interface PaymentChannel { /** * 支付 */ PaymentResponse pay(PaymentChannelRequest request); /** * 退款 */ RefundResponse refund(RefundRequest request); /** * 查询支付结果 */ QueryResponse query(QueryRequest request); /** * 处理异步通知 */ NotifyResponse handleNotify(NotifyRequest request); } @Service("ALIPAY_APP") public class AlipayAppChannel implements PaymentChannel { @Override public PaymentResponse pay(PaymentChannelRequest request) { // 构造支付宝APP支付参数 AlipayTradeAppPayRequest alipayRequest = new AlipayTradeAppPayRequest(); // 设置业务参数 // 调用支付宝SDK // 返回支付参数 return buildPaymentResponse(alipayResponse); } @Override public NotifyResponse handleNotify(NotifyRequest request) { // 验证签名 if (!verifySignature(request)) { return NotifyResponse.fail("签名验证失败"); } // 处理支付结果 processPaymentResult(request); return NotifyResponse.success(); } }

支付幂等性保障

幂等是针对重复请求的,支付系统一般会面临以下几个重复请求的场景:

  • 用户多次点击支付按钮:在网络较差或系统过载情况下,用户由于不确定交易是否完成而重复点击。
  • 自动重试机制:系统在超时或失败时重试请求,可能导致同一支付多次尝试。
  • 网络数据包重复:数据包在网络传输过程中,复制出了多份,导致支付平台收到多次一模一样的请求。
  • 异常恢复:在系统升级或崩溃后,未决事务需要根据已有记录恢复和完成。内部系统重发操作。

幂等解决方案

image.png

所谓业务幂等,就是由各域自己把唯一性的交易ID作为数据库唯一索引,这样可以保证不会重复处理。

在数据库前面可以加一层缓存来提高性能,但是缓存只用于查询,查到数据认为就返回幂等成功,但是但不到,需要尝试插入数据库,插入成功后再刷新数据到缓存。

为什么要使用数据库的唯一索引做为兜底?

是因为缓存是可能失效的。

在面临时经常有同学只回答到“使用redis分布式锁来实现幂等”,这是不对的。因为缓存有可能失效,分布式锁只是用于防并发操作的一种手段,无法根本性解决幂等问题,幂等一定是依赖数据库的唯一索引解决。

支付接口必须保证幂等性,防止重复支付:

java
@Service public class PaymentIdempotentService { @Autowired private RedisTemplate redisTemplate; /** * 支付幂等性校验 */ public boolean checkIdempotent(String paymentOrderNo, String idempotentKey) { String redisKey = "payment:idempotent:" + paymentOrderNo; // 使用Lua脚本保证原子性 String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " + " return 1 " + "else " + " redis.call('set', KEYS[1], ARGV[1], 'EX', 3600) " + " return 0 " + "end"; Long result = redisTemplate.execute( new DefaultRedisScript<>(luaScript, Long.class), Collections.singletonList(redisKey), idempotentKey ); return result == 1; } }

大部分简单的支付系统只要有业务幂等基本也够用了。

金额处理规范

对于研发经验不足的团队而言,经常会犯以下几种错误:

  • 没有定义统一的Money类,各系统间使用BigDecimal、double、long等数据类型进行金额处理及存储。
  • 定义了统一的Money类,但是写代码时不严格遵守,仍然有些代码使用BigDecimal、double、long等数据类型进行金额处理。
  • 手动对金额进行加、减、乘、除运算,单位(元与分)换算。

带来的后果,通常就是资金损失,再细化一下,最常见的情况有下面3种:

  1. 手动做单位换算导致金额被放大或缩小100倍。

    比如大家规定传的是元,但是其中有位同学忘记了,以为传的是分,外部渠道要求传元,就手动乘以100。或者反过来。

    还有一种情况,部分币种比如日元最小单元就是元,假如系统约定传的是分,外部渠道要求传元,就可能在网关处理时手动乘以100。

  2. 1分钱归属问题。比如结算给商家,或计算手续费时,碰到除不尽时,使用四舍五入,还是向零舍入,还是银行家舍入?这取决于财务策略。

  3. 精度丢失。在大金额时,double有可能会有精度丢失问题。

最佳实践:

image.png

  • 制定适用于公司业务的Money类来统一处理金额。
  • 在入口网关接收到请求后,就转换为Money类。
  • 所有内部应用的金额处理,强制全部使用Money类运算、传输,禁止自己手动加减乘除、单位换算(比如元到分)。
  • 数据库使用 DECIMAL 类型保存,保存单位为元。
  • 在出口网关外发时,再根据外部接口文档要求,转换成使用指定的单位。有些是元,有些是分(最小货币单位)

业务ID生成规则

数据库一般都会设计一个自增ID作为主键,同时还会设计一个能唯一标识一笔业务的ID,这就是所谓的业务ID(也称业务键)。比如收单域的收单单号。

也有人采用所谓雪花算法,但其实不适用于支付场景。

下面以32位的支付系统业务ID生成为例说明。实际应用时可灵活调整。

image.png

  • 第1-8位:日期。通过单号一眼能看出是哪天的交易。

  • 第9位:数据版本。用于单据号的升级。

  • 第10位:系统版本。用于内部系统版本升级,尤其是不兼容升级的时候,老业务使用老的系统处理,新业务使用新系统处理。

  • 第11-13位:系统标识码。支付系统内部每个域分配一段,由各域自行再分配给内部系统。比如010是收单核心,012是结算核心。

  • 第14-15位:业务标识位。由各域内部定,比如00-15代表支付类业务,01支付,02预授权,03请款等。

  • 第16-17位:机房位。用于全球化部署。

  • 第18-19位:用户分库位。支持百库。

  • 第20-21位:用户分表位。支持百表。

  • 第22位:预发生产标识位。比如0代表预发环境,1代表生产环境。

  • 第23-24位:预留。各域根据实际情况扩展使用。

  • 第24-32位:序列号空间。一亿规模,循环使用。一个机房一天一亿笔是很大的规模了。如果不够用,可以扩展到第24位,到十亿规模。

日志规范

只要在公司写过代码,就一定打印过日志,但经常发现一些工作多年的工程师打印的日志也是乱七八糟的。我曾经在一家头部互联网公司接手过一个上线一年多的业务,相关日志一开始就没有设计好,导致很多监控无法实现,出了线上问题也不知道,最后只能安排工程师返工改造相关的日志。

我们要明白日志是用来做什么的。只是先弄明白做事的目的,我们才能更好把事情做对。在我看来,日志有两个核心的作用:

  • 监控,诊断系统或业务是否存在问题;
  • 排查问题。

对于监控而言,我们需要知道几个核心的数据:业务/接口的请求量、成功量、成功率、耗时,系统返回码、业务返回码,异常信息等。对于排查问题而言,我们需要有出入参、中间处理数据的上下文、报错的上下文等。

image.png

接下来,基于上面的分析,我们就清楚我们应该有几种日志:

  1. 接口摘要日志。监控接口的请求量、成功量、耗时、返回码等。使用固定格式,需要打印:时间、接口名称、结果(成功/失败)、返回码、耗时等基本信息就足够。
  2. 业务摘要日志。监控业务的请求量、成功量、核心业务信息、返回码等。使用固定格式,需要打印:时间、业务类型、上一步状态、当前状态、返回码、核心业务信息(不同业务有不同的核心业务信息,比如流入,就有支付金额/退款金额,卡品牌,卡BIN等)。
  3. 详细日志。用于排查问题,不用于监控。格式不固定。主要包括时间、接口、入参、出参、中间处理数据输入、异常的堆栈信息等。
  4. 系统异常日志。同时用于监控。格式固定。需要打印:时间、错误码、错误信息、堆栈信息等。

异步通知处理

支付渠道的异步通知是确认支付结果的最终依据:

java
@Service public class PaymentNotifyService { /** * 处理支付通知 */ @Transactional(rollbackFor = Exception.class) public void processNotify(PaymentNotifyDTO notifyDTO) { // 1. 验证通知合法性 if (!verifyNotify(notifyDTO)) { throw new PaymentException("通知验证失败"); } // 2. 查询支付订单 PaymentOrder paymentOrder = paymentOrderDAO.selectByOrderNo(notifyDTO.getPaymentOrderNo()); if (paymentOrder == null) { throw new PaymentException("支付订单不存在"); } // 3. 检查订单状态,避免重复处理 if (paymentOrder.getPaymentStatus() == PaymentStatus.SUCCESS) { return; } // 4. 更新支付订单状态 updatePaymentOrderStatus(paymentOrder, notifyDTO); // 5. 更新业务订单状态 updateBusinessOrderStatus(paymentOrder.getBusinessOrderNo()); // 6. 记录通知日志 saveNotifyLog(notifyDTO); } }

分布式事务解决方案

支付系统涉及多个服务的数据一致性,需要采用合适的分布式事务方案:

基于本地消息表的最终一致性:

java
@Service public class PaymentTransactionService { /** * 支付成功后的业务处理 */ @Transactional(rollbackFor = Exception.class) public void handlePaymentSuccess(String paymentOrderNo) { // 1. 更新支付订单状态 updatePaymentOrder(paymentOrderNo, PaymentStatus.SUCCESS); // 2. 插入本地消息表 EventMessage message = buildOrderPaidMessage(paymentOrderNo); eventMessageDAO.insert(message); // 3. 更新积分、库存等其他业务状态 updateRelatedBusiness(paymentOrderNo); } /** * 消息补偿任务 */ @Scheduled(fixedDelay = 30000) public void compensateMessages() { List<EventMessage> pendingMessages = eventMessageDAO.selectPendingMessages(); for (EventMessage message : pendingMessages) { try { // 重新投递消息 eventPublisher.publishEvent(message); // 更新消息状态为已发送 eventMessageDAO.updateStatus(message.getId(), MessageStatus.SENT); } catch (Exception e) { log.error("消息补偿失败: {}", message.getId(), e); } } } }

为什么有些企业不用分布式事务?

分布式事务是个好东西,但是复杂度也高,还经常出现所谓的事务悬挂问题,且虽然各家都号称简单易用,对业务代码侵入少,但事实并非如此。

所以我个人更倾向于避免使用分布式事务解决方案,而是采用最终一致性来解决。对大部分中小公司来说,最终一致性已经够用。

支付对账系统

每日对账是保障资金安全的重要手段:

java
@Service public class ReconciliationService { /** * 执行对账 */ public void executeReconciliation(Date checkDate) { // 1. 下载渠道对账单 List<ChannelBill> channelBills = downloadChannelBills(checkDate); // 2. 查询系统支付记录 List<PaymentOrder> systemOrders = getSystemPaymentOrders(checkDate); // 3. 对账匹配 ReconciliationResult result = matchBills(channelBills, systemOrders); // 4. 处理差异订单 handleDifferences(result.getDifferences()); // 5. 生成对账报告 generateReport(result); } /** * 账单匹配 */ private ReconciliationResult matchBills(List<ChannelBill> channelBills, List<PaymentOrder> systemOrders) { ReconciliationResult result = new ReconciliationResult(); // 以渠道账单为准进行匹配 for (ChannelBill channelBill : channelBills) { PaymentOrder systemOrder = findSystemOrder(systemOrders, channelBill); if (systemOrder == null) { // 渠道有系统无 - 长款 result.addDifference(ReconciliationDiff.extra(channelBill)); } else if (!amountEquals(channelBill, systemOrder)) { // 金额不一致 result.addDifference(ReconciliationDiff.amountMismatch(channelBill, systemOrder)); } else { // 匹配成功 result.addMatch(channelBill, systemOrder); } } // 检查系统有渠道无 - 短款 for (PaymentOrder systemOrder : systemOrders) { if (!isMatched(systemOrder)) { result.addDifference(ReconciliationDiff.missing(systemOrder)); } } return result; } }

支付渠道降级策略

服务高可用设计,当某个支付渠道出现故障时,自动切换到备用渠道:

java
@Service public class PaymentCircuitBreaker { private Map<String, CircuitBreakerStats> statsMap = new ConcurrentHashMap<>(); /** * 获取可用的支付渠道 */ public List<String> getAvailableChannels(PaymentRequest request) { return statsMap.entrySet().stream() .filter(entry -> isChannelAvailable(entry.getKey(), entry.getValue())) .map(Map.Entry::getKey) .sorted(Comparator.comparing(this::getChannelPriority)) .collect(Collectors.toList()); } /** * 判断渠道是否可用 */ private boolean isChannelAvailable(String channel, CircuitBreakerStats stats) { // 基于失败率、超时率等指标判断 return stats.getFailureRate() < 0.5 // 失败率低于50% && stats.getTimeoutRate() < 0.3 // 超时率低于30% && !stats.isCircuitOpen(); // 断路器未打开 } /** * 记录调用结果 */ public void recordCallResult(String channel, boolean success, long cost) { CircuitBreakerStats stats = statsMap.computeIfAbsent( channel, k -> new CircuitBreakerStats() ); stats.recordCall(success, cost); } }

数据一致性保障

通过多种机制保障支付数据的一致性:

java
@Component public class PaymentConsistencyChecker { /** * 定时检查支付状态不一致的订单 */ @Scheduled(cron = "0 */5 * * * ?") public void checkInconsistentOrders() { // 查询支付中状态超过一定时间的订单 List<PaymentOrder> processingOrders = paymentOrderDAO.selectLongTimeProcessingOrders(); for (PaymentOrder order : processingOrders) { try { // 主动查询支付渠道确认状态 QueryResponse response = paymentChannel.query( new QueryRequest(order.getPaymentOrderNo()) ); if (response.isSuccess()) { // 更新为成功状态 updatePaymentSuccess(order, response); } else if (response.isFailed()) { // 更新为失败状态 updatePaymentFailed(order, response); } // 仍然处理中则忽略 } catch (Exception e) { log.error("支付状态检查失败: {}", order.getPaymentOrderNo(), e); } } } }

监控与告警

支付系统需要完善的监控体系来保障稳定性:

关键监控指标:

  • 支付成功率:各渠道支付成功比率

  • 支付耗时:支付各环节耗时分布

  • 失败原因分布:各类失败原因的统计

  • 渠道可用性:各支付渠道的健康状态

  • 资金一致性:系统与渠道资金差异告警

java
@Component public class PaymentMetrics { private final MeterRegistry meterRegistry; public void recordPaymentResult(String channel, boolean success, long cost) { // 记录支付结果 Counter.builder("payment.result") .tag("channel", channel) .tag("success", String.valueOf(success)) .register(meterRegistry) .increment(); // 记录支付耗时 Timer.builder("payment.cost") .tag("channel", channel) .register(meterRegistry) .record(cost, TimeUnit.MILLISECONDS); } }

支付安全

支付安全核心关注点

image.png

支付安全是一个很大的范畴,但我们一般只需要重点关注以下几个核心点就够:

1. 敏感信息安全存储。

对个人和商户/渠道的敏感信息进行安全存储。

个人敏感信息包括身份证信息、支付卡明文数据和密码等,而商户/渠道的敏感信息则涉及商户登录/操作密码、渠道证书密钥等。

2. 交易信息安全传输。

确保客户端与支付系统服务器之间、商户系统与支付系统之间、支付系统内部服务器与服务器之间、支付系统与银行之间的数据传输安全。这包括采用加密技术等措施来保障数据传输过程中的安全性。

3. 交易信息的防篡改与防抵赖。 确保交易信息的完整性和真实性,防止交易信息被篡改或者被抵赖。一笔典型的交易,通常涉及到用户、商户、支付机构、银行四方,确保各方发出的信息没有被篡改也无法被抵赖。

4. 欺诈交易防范。 识别并防止欺诈交易,包括套现、洗钱等违规操作,以及通过识别用户信息泄露和可疑交易来保护用户资产的安全。这一方面通常由支付风控系统负责。

5. 服务可用性。 防范DDoS攻击,确保支付系统的稳定运行和服务可用性。通过部署防火墙、入侵检测系统等技术手段,及时发现并应对可能的DDoS攻击,保障支付服务的正常进行。

极简支付安全

支付安全是一个综合性的系统工程,除了技术手段外,还需要建立健全的安全制度和合规制度,而后两者通常被大部分人所忽略。

下图是一个极简版的支付安全大图,包含了支付安全需要考虑的核心要点。

image.png

制度是基础。

哪种场景下需要加密存储,加密需要使用什么算法,密钥长度最少需要多少位,哪些场景下需要做签名验签,这些都是制度就明确了的。制度通常分为行业制度和内部安全制度。行业制度通常是国家层面制定的法律法规,比如《网络安全法》、《支付业务管理办法》等。内部安全制度通常是公司根据自身的业务和能力建立的制度,小公司可能就没有。

技术手段主要围绕四个目标:

1)敏感数据安全存储。

2)交易安全传输。

3)交易的完整性和真实性。

4)交易的合法性(无欺诈)。

对应的技术手段有:

  • 敏感信息安全存储:采用加密技术对个人和商户/渠道的敏感信息进行加密存储,限制敏感信息的访问权限,防止未授权的访问和泄露。
  • 交易信息安全传输:使用安全套接字层(SSL)或传输层安全性协议(TLS)等加密技术,确保数据在传输过程中的机密性和完整性。
  • 交易的完整性和真实性:采用数字签名技术和身份认证技术确保交易信息的完整性和真实性,对交易信息进行记录和审计,建立可追溯的交易日志,以应对可能出现的交易篡改或抵赖情况。
  • 防范欺诈交易:通过支付风控系统,及时识别和阻止可疑交易行为。
  • 服务可用性:部署流量清洗设备和入侵检测系统,及时发现并阻止恶意流量,确保支付系统的稳定运行和服务可用性,抵御DDoS攻击。

总结

支付系统作为电商平台的核心组件,其设计需要重点关注以下几个方面:

  • 高可用性:通过多渠道冗余、断路器降级、异步化处理等手段保障系统可用性

  • 数据一致性:采用分布式事务、最终一致性、对账补偿等机制确保资金安全

  • 安全性:加强身份验证、参数签名、风险控制等安全措施

  • 扩展性:通过模块化设计、策略模式等支持新支付渠道的快速接入

  • 可观测性:建立完善的监控、日志、告警体系,快速发现和定位问题

一个健壮的支付系统不仅需要技术上的严谨设计,还需要业务流程上的周密考虑。只有在技术和业务的共同保障下,才能为用户提供安全、便捷、稳定的支付体验,为平台创造更大的商业价值。

本文作者:柳始恭

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!