Skip to content

如何设计支付系统

一、问题描述

1.1 业务背景

支付系统是电商、金融平台的核心,直接涉及资金安全。

典型应用

  • 电商支付:淘宝、京东支付
  • 第三方支付:支付宝、微信支付
  • 金融支付:银行转账、理财

核心要求

  • 资金安全:一分钱都不能错
  • 数据一致性:强一致性
  • 高可用:99.99%以上

1.2 核心功能

基础功能

  1. 发起支付:创建支付订单
  2. 支付确认:完成支付
  3. 支付回调:接收支付结果
  4. 支付查询:查询支付状态

进阶功能

  1. 退款:支付退回
  2. 对账:定时对账
  3. 风控:异常检测

1.3 技术挑战

幂等性

用户点击支付,网络超时重试
如何保证不重复扣款?

一致性

扣款成功,但订单状态未更新
如何保证状态一致?

资金安全

如何保证账户余额不会错?
如何防止黑客攻击?

1.4 面试考察点

  • 支付流程:完整的状态机设计
  • 幂等性:Token机制
  • 账户模型:借贷记账法
  • 对账系统:定时对账和差错处理

二、支付流程

2.1 完整流程图

mermaid
sequenceDiagram
    participant U as 用户
    participant M as 商户系统
    participant P as 支付系统
    participant C as 支付渠道
    
    U->>M: 1. 提交订单
    M->>P: 2. 创建支付订单
    P-->>M: 3. 返回支付订单号
    
    M->>P: 4. 发起支付
    P->>P: 5. 检查幂等Token
    P->>P: 6. 扣减账户余额
    P->>C: 7. 调用第三方支付
    C-->>P: 8. 返回支付结果
    
    alt 支付成功
        P->>M: 9. 异步回调通知
        M->>M: 10. 更新订单状态
        M-->>P: 11. 确认收到回调
        P-->>U: 12. 支付成功
    else 支付失败
        P->>P: 13. 回滚账户余额
        P-->>U: 14. 支付失败
    end

2.2 支付状态机

mermaid
stateDiagram-v2
    [*] --> 待支付
    待支付 --> 支付中: 发起支付
    支付中 --> 支付成功: 支付完成
    支付中 --> 支付失败: 支付失败
    支付成功 --> 已退款: 退款
    支付失败 --> 待支付: 重新支付

三、账户模型

3.1 借贷记账法

原理:每笔交易都有借方和贷方,借贷相等。

账户表

sql
CREATE TABLE account (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_id BIGINT UNIQUE NOT NULL,
    balance BIGINT DEFAULT 0 COMMENT '余额(分)',
    frozen_balance BIGINT DEFAULT 0 COMMENT '冻结金额(分)',
    version INT DEFAULT 0 COMMENT '版本号(乐观锁)',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    
    KEY idx_user (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
COMMENT='账户表';

流水表(关键):

sql
CREATE TABLE account_log (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    trade_no VARCHAR(64) UNIQUE NOT NULL COMMENT '交易流水号',
    user_id BIGINT NOT NULL,
    amount BIGINT NOT NULL COMMENT '金额(分)',
    balance_before BIGINT NOT NULL COMMENT '交易前余额',
    balance_after BIGINT NOT NULL COMMENT '交易后余额',
    trade_type TINYINT NOT NULL COMMENT '交易类型 1充值 2消费 3退款',
    biz_type VARCHAR(32) COMMENT '业务类型',
    biz_id VARCHAR(64) COMMENT '业务ID',
    remark VARCHAR(500),
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    
    KEY idx_user (user_id, create_time),
    KEY idx_trade_no (trade_no)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
COMMENT='账户流水表';

3.2 借贷记账示例

场景:用户A向用户B转账100元

go
// 借方(A账户减少)
{
    UserID:        A,
    Amount:        -100,
    BalanceBefore: 1000,
    BalanceAfter:  900,
    TradeType:     2,  // 消费
}

// 贷方(B账户增加)
{
    UserID:        B,
    Amount:        100,
    BalanceBefore: 500,
    BalanceAfter:  600,
    TradeType:     1,  // 充值
}

// 校验:借贷相等
Sum(借方) + Sum(贷方) = -100 + 100 = 0

四、幂等性设计

4.1 Token机制

流程

  1. 发起支付前,获取幂等Token
  2. 提交支付时,携带Token
  3. 支付系统校验Token,成功后立即删除
  4. 重复请求时,Token已不存在,返回"处理中"

实现

go
// 1. 获取Token
func (s *PaymentService) GetIdempotentToken(userID int64) (string, error) {
    token := generateToken()  // 生成UUID
    
    // 存储到Redis,5分钟有效
    key := fmt.Sprintf("payment:token:%s", token)
    s.redis.Set(ctx, key, userID, 5*time.Minute)
    
    return token, nil
}

// 2. 校验并消费Token
func (s *PaymentService) ConsumeToken(token string, userID int64) error {
    key := fmt.Sprintf("payment:token:%s", token)
    
    // 使用Lua脚本原子性检查+删除
    script := `
        local val = redis.call('GET', KEYS[1])
        if not val then
            return 0  -- Token不存在或已使用
        end
        if val ~= ARGV[1] then
            return -1  -- 用户不匹配
        end
        redis.call('DEL', KEYS[1])
        return 1  -- 成功
    `
    
    result, err := s.redis.Eval(ctx, script, []string{key}, userID).Int()
    if err != nil {
        return err
    }
    
    if result == 0 {
        return errors.New("Token已使用,请勿重复提交")
    }
    if result == -1 {
        return errors.New("Token不匹配")
    }
    
    return nil
}

4.2 唯一键约束

sql
-- 支付订单表:trade_no唯一
CREATE TABLE payment_order (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    trade_no VARCHAR(64) UNIQUE NOT NULL,
    ...
);

-- 插入时自动去重
INSERT INTO payment_order (trade_no, ...) VALUES (?, ...);
-- 重复插入会报错:Duplicate entry

五、核心实现

5.1 Go实现

点击查看完整实现
go
package payment

import (
    "context"
    "fmt"
    "time"
    
    "gorm.io/gorm"
)

type PaymentService struct {
    db    *gorm.DB
    redis *redis.Client
}

type PaymentOrder struct {
    ID         int64
    TradeNo    string  // 交易流水号
    OutTradeNo string  // 商户订单号
    UserID     int64
    Amount     int64   // 金额(分)
    Channel    string  // 支付渠道
    Status     int8    // 1待支付 2支付中 3成功 4失败
    CreateTime time.Time
}

type Account struct {
    ID            int64
    UserID        int64
    Balance       int64
    FrozenBalance int64
    Version       int
}

type AccountLog struct {
    ID            int64
    TradeNo       string
    UserID        int64
    Amount        int64
    BalanceBefore int64
    BalanceAfter  int64
    TradeType     int8
    CreateTime    time.Time
}

// CreatePaymentOrder 创建支付订单
func (s *PaymentService) CreatePaymentOrder(ctx context.Context, req *CreateOrderReq) (*PaymentOrder, error) {
    order := &PaymentOrder{
        TradeNo:    generateTradeNo(),
        OutTradeNo: req.OutTradeNo,
        UserID:     req.UserID,
        Amount:     req.Amount,
        Channel:    req.Channel,
        Status:     1,  // 待支付
        CreateTime: time.Now(),
    }
    
    err := s.db.Create(order).Error
    if err != nil {
        return nil, err
    }
    
    return order, nil
}

// Pay 发起支付
func (s *PaymentService) Pay(ctx context.Context, req *PayReq) error {
    // 1. 校验幂等Token
    err := s.ConsumeToken(req.IdempotentToken, req.UserID)
    if err != nil {
        return err
    }
    
    // 2. 查询支付订单
    var order PaymentOrder
    err = s.db.Where("trade_no = ?", req.TradeNo).First(&order).Error
    if err != nil {
        return err
    }
    
    // 3. 检查订单状态
    if order.Status != 1 {
        return fmt.Errorf("订单状态异常")
    }
    
    // 4. 更新订单状态为支付中
    s.db.Model(&order).Update("status", 2)
    
    // 5. 扣减账户余额
    err = s.DeductBalance(ctx, req.UserID, order.Amount, order.TradeNo)
    if err != nil {
        // 回滚订单状态
        s.db.Model(&order).Update("status", 4)
        return err
    }
    
    // 6. 调用第三方支付(省略)
    // ...
    
    // 7. 更新订单状态为成功
    s.db.Model(&order).Update("status", 3)
    
    return nil
}

// DeductBalance 扣减账户余额
func (s *PaymentService) DeductBalance(ctx context.Context, userID, amount int64, tradeNo string) error {
    return s.db.Transaction(func(tx *gorm.DB) error {
        // 1. 锁定账户行
        var account Account
        err := tx.Set("gorm:query_option", "FOR UPDATE").
            Where("user_id = ?", userID).
            First(&account).Error
        if err != nil {
            return err
        }
        
        // 2. 检查余额
        if account.Balance < amount {
            return fmt.Errorf("余额不足")
        }
        
        // 3. 扣减余额(乐观锁)
        affected := tx.Model(&account).
            Where("version = ?", account.Version).
            Updates(map[string]interface{}{
                "balance": gorm.Expr("balance - ?", amount),
                "version": gorm.Expr("version + 1"),
            }).RowsAffected
        
        if affected == 0 {
            return fmt.Errorf("账户版本冲突,请重试")
        }
        
        // 4. 记录流水
        log := &AccountLog{
            TradeNo:       tradeNo,
            UserID:        userID,
            Amount:        -amount,  // 负数表示支出
            BalanceBefore: account.Balance,
            BalanceAfter:  account.Balance - amount,
            TradeType:     2,  // 消费
            CreateTime:    time.Now(),
        }
        err = tx.Create(log).Error
        if err != nil {
            return err
        }
        
        return nil
    })
}

// PaymentCallback 支付回调
func (s *PaymentService) PaymentCallback(ctx context.Context, req *CallbackReq) error {
    // 1. 验证签名(省略)
    // ...
    
    // 2. 查询订单
    var order PaymentOrder
    err := s.db.Where("trade_no = ?", req.TradeNo).First(&order).Error
    if err != nil {
        return err
    }
    
    // 3. 幂等性检查
    if order.Status == 3 {
        return nil  // 已处理,直接返回成功
    }
    
    // 4. 更新订单状态
    if req.PayResult == "SUCCESS" {
        s.db.Model(&order).Update("status", 3)
        
        // 5. 通知商户(异步)
        s.notifyMerchant(order.OutTradeNo, "SUCCESS")
    } else {
        s.db.Model(&order).Update("status", 4)
        
        // 回滚账户余额
        s.RefundBalance(ctx, order.UserID, order.Amount, order.TradeNo)
    }
    
    return nil
}

// Refund 退款
func (s *PaymentService) Refund(ctx context.Context, tradeNo string) error {
    // 1. 查询原支付订单
    var order PaymentOrder
    err := s.db.Where("trade_no = ?", tradeNo).First(&order).Error
    if err != nil {
        return err
    }
    
    // 2. 检查订单状态
    if order.Status != 3 {
        return fmt.Errorf("订单未支付成功,无法退款")
    }
    
    // 3. 调用第三方退款接口(省略)
    // ...
    
    // 4. 退回账户余额
    return s.RefundBalance(ctx, order.UserID, order.Amount, tradeNo+"-REFUND")
}

// RefundBalance 退款到账户
func (s *PaymentService) RefundBalance(ctx context.Context, userID, amount int64, tradeNo string) error {
    return s.db.Transaction(func(tx *gorm.DB) error {
        // 1. 增加余额
        var account Account
        tx.Where("user_id = ?", userID).First(&account)
        
        tx.Model(&account).Updates(map[string]interface{}{
            "balance": gorm.Expr("balance + ?", amount),
            "version": gorm.Expr("version + 1"),
        })
        
        // 2. 记录流水
        log := &AccountLog{
            TradeNo:       tradeNo,
            UserID:        userID,
            Amount:        amount,  // 正数表示收入
            BalanceBefore: account.Balance,
            BalanceAfter:  account.Balance + amount,
            TradeType:     3,  // 退款
            CreateTime:    time.Now(),
        }
        return tx.Create(log).Error
    })
}

// Reconcile 对账
func (s *PaymentService) Reconcile(ctx context.Context, date string) error {
    // 1. 从第三方获取对账单
    thirdPartyOrders := s.fetchThirdPartyOrders(date)
    
    // 2. 从数据库获取订单
    var localOrders []PaymentOrder
    s.db.Where("DATE(create_time) = ?", date).Find(&localOrders)
    
    // 3. 对比差异
    thirdMap := make(map[string]int64)
    for _, o := range thirdPartyOrders {
        thirdMap[o.TradeNo] = o.Amount
    }
    
    localMap := make(map[string]int64)
    for _, o := range localOrders {
        localMap[o.TradeNo] = o.Amount
    }
    
    // 4. 找出差异订单
    var diffOrders []string
    
    // 本地有,第三方没有
    for tradeNo := range localMap {
        if _, exists := thirdMap[tradeNo]; !exists {
            diffOrders = append(diffOrders, tradeNo)
        }
    }
    
    // 第三方有,本地没有
    for tradeNo := range thirdMap {
        if _, exists := localMap[tradeNo]; !exists {
            diffOrders = append(diffOrders, tradeNo)
        }
    }
    
    // 5. 告警差异订单
    if len(diffOrders) > 0 {
        s.alertDiffOrders(diffOrders)
    }
    
    return nil
}

func generateTradeNo() string {
    return fmt.Sprintf("PAY%d", time.Now().UnixNano())
}

5.2 Java实现

java
@Service
public class PaymentService {
    
    @Autowired
    private PaymentOrderMapper orderMapper;
    
    @Autowired
    private AccountMapper accountMapper;
    
    @Autowired
    private AccountLogMapper logMapper;
    
    /**
     * 发起支付
     */
    @Transactional
    public void pay(PayRequest request) {
        // 1. 校验幂等Token
        consumeToken(request.getIdempotentToken(), request.getUserId());
        
        // 2. 查询支付订单
        PaymentOrder order = orderMapper.findByTradeNo(request.getTradeNo());
        if (order.getStatus() != 1) {
            throw new BusinessException("订单状态异常");
        }
        
        // 3. 更新订单状态为支付中
        orderMapper.updateStatus(order.getId(), 2);
        
        try {
            // 4. 扣减账户余额
            deductBalance(request.getUserId(), order.getAmount(), order.getTradeNo());
            
            // 5. 调用第三方支付
            // ...
            
            // 6. 更新订单状态为成功
            orderMapper.updateStatus(order.getId(), 3);
            
        } catch (Exception e) {
            // 回滚订单状态
            orderMapper.updateStatus(order.getId(), 4);
            throw e;
        }
    }
    
    /**
     * 扣减账户余额
     */
    private void deductBalance(Long userId, Long amount, String tradeNo) {
        // 1. 锁定账户
        Account account = accountMapper.lockByUserId(userId);
        
        // 2. 检查余额
        if (account.getBalance() < amount) {
            throw new BusinessException("余额不足");
        }
        
        // 3. 扣减余额(乐观锁)
        int affected = accountMapper.deductBalance(
            userId, 
            amount, 
            account.getVersion()
        );
        
        if (affected == 0) {
            throw new BusinessException("账户版本冲突,请重试");
        }
        
        // 4. 记录流水
        AccountLog log = new AccountLog();
        log.setTradeNo(tradeNo);
        log.setUserId(userId);
        log.setAmount(-amount);
        log.setBalanceBefore(account.getBalance());
        log.setBalanceAfter(account.getBalance() - amount);
        log.setTradeType((byte) 2);
        
        logMapper.insert(log);
    }
}

六、对账系统

6.1 对账流程

mermaid
graph LR
    A[定时任务] --> B[获取第三方对账单]
    A --> C[获取本地订单]
    B --> D[数据对比]
    C --> D
    D --> E{是否一致?}
    E -->|是| F[对账完成]
    E -->|否| G[记录差异]
    G --> H[人工处理]

6.2 差错处理

差错类型

  1. 长款:第三方有,本地没有 → 补单
  2. 短款:本地有,第三方没有 → 查询第三方状态
  3. 金额不符:金额不一致 → 以第三方为准,调整本地

七、面试要点

7.1 常见追问

Q1: 如何保证支付幂等性?

A: Token机制 + 唯一键约束

  • Token机制:获取Token → 提交时消费 → 重复请求Token已失效
  • 唯一键约束:trade_no UNIQUE,重复插入会报错

Q2: 账户模型如何设计?

A: 借贷记账法

  • 账户表:记录余额
  • 流水表:记录每笔交易(借贷必相等)
  • 校验:Sum(借方) + Sum(贷方) = 0

Q3: 如何保证资金安全?

A: 多重保障:

  1. 数据库事务ACID
  2. 乐观锁(版本号)
  3. 流水记录(可追溯)
  4. 定时对账(发现差异)

Q4: 对账系统如何设计?

A:

  1. 定时获取第三方对账单
  2. 对比本地订单
  3. 记录差异订单
  4. 人工处理差错

7.2 扩展知识点

相关场景题

八、总结

支付系统设计要点:

  1. 幂等性:Token机制 + 唯一键约束
  2. 账户模型:借贷记账法
  3. 资金安全:事务 + 乐观锁 + 流水
  4. 对账系统:定时对账 + 差错处理

面试重点

  • 能画出完整的支付流程图
  • 能解释幂等Token机制
  • 能说出借贷记账法原理
  • 能设计对账系统

参考资料

  • 支付宝架构演进
  • 微信支付技术分享

正在精进