尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

支付中心的钱包类业务应该怎么设计

支付中心的钱包类业务应该怎么设计
📅 发布时间:2026/6/19 11:24:18

钱包类业务在支付行业里有一些比较固定的模式(无论是支付宝余额宝、微信零钱,还是 Stripe Balance / Paytm Wallet),基本设计目标是:

  1. 余额和资金安全:必须有严格的账实一致、幂等和防篡改能力。

  2. 高并发读写:充值/消费/退款频繁,要求快速的扣减和回滚能力。

  3. 清晰的流水:任何一笔资金变动必须有对应的流水,支持对账和审计。


📌 表设计(MySQL 示例)

1. 钱包账户表(核心账户信息)

CREATE TABLE wallet_account (id BIGINT PRIMARY KEY AUTO_INCREMENT,user_id BIGINT NOT NULL UNIQUE COMMENT '用户ID',balance DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '可用余额',frozen_balance DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '冻结余额',status TINYINT NOT NULL DEFAULT 1 COMMENT '账户状态 1=正常 0=冻结',-- 弃用 version BIGINT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',hash_sign VARCHAR(128) COMMENT '数据校验签名(防篡改水印)',created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,UNIQUE KEY uk_user (user_id)
) ENGINE=InnoDB;

要点:

  • balance 和 frozen_balance 一般放在 同一个表,保证原子性更新。

  • version 做乐观锁,避免并发扣减错误。

  • hash_sign 是水印字段(例如 MD5(user_id + balance + frozen_balance + secret_salt)),在数据校验任务中比对,防止 DBA/黑客直接篡改。


2. 钱包流水表(账务流水)

CREATE TABLE wallet_ledger (id BIGINT PRIMARY KEY AUTO_INCREMENT,wallet_id BIGINT NOT NULL,txn_id VARCHAR(64) NOT NULL COMMENT '关联的交易单号/支付单号',biz_type VARCHAR(32) NOT NULL COMMENT '业务类型: RECHARGE, PAY, REFUND, FREEZE, UNFREEZE',change_amount DECIMAL(18,2) NOT NULL COMMENT '变动金额(正=增加,负=减少)',balance_after DECIMAL(18,2) NOT NULL COMMENT '变动后的可用余额',frozen_after DECIMAL(18,2) NOT NULL COMMENT '变动后的冻结余额',remark VARCHAR(255),created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,INDEX idx_wallet (wallet_id),UNIQUE KEY uk_txn (txn_id, biz_type) -- 保证幂等
) ENGINE=InnoDB;

要点:

  • 每笔操作必须落流水,账随流转。

  • biz_type 用来区分充值、支付、退款、冻结、解冻。

  • uk_txn 保证幂等(相同交易单号不会重复入账)。


3. 钱包交易订单表(业务订单)

CREATE TABLE wallet_transaction (id BIGINT PRIMARY KEY AUTO_INCREMENT,txn_id VARCHAR(64) NOT NULL UNIQUE COMMENT '钱包交易单号',user_id BIGINT NOT NULL,type VARCHAR(32) NOT NULL COMMENT '类型: RECHARGE, CONSUME, REFUND',amount DECIMAL(18,2) NOT NULL COMMENT '订单金额',status VARCHAR(16) NOT NULL COMMENT 'INIT, SUCCESS, FAILED, PROCESSING',channel VARCHAR(32) COMMENT '支付通道(仅充值时有)',related_order_id VARCHAR(64) COMMENT '业务订单ID, 如电商订单号',created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB;

要点:

  • 钱包交易订单表 和 流水表 是分开的:

    • 订单表:对外业务维度(如一笔充值、一笔消费)。

    • 流水表:资金原子动作(可能一笔订单拆成多条流水,比如冻结→支付→解冻)。

  • 状态机驱动(INIT → PROCESSING → SUCCESS/FAILED)。


📌 流程逻辑

1. 充值(RECHARGE)

  1. 用户提交充值请求,生成 wallet_transaction(type=RECHARGE, status=INIT)。

  2. 调用支付通道(如银行卡/支付宝充值),异步回调成功后:

    • 开启事务:

      • 更新 wallet_account.balance = balance + amount。

      • 写入 wallet_ledger。

      • 更新 wallet_transaction.status=SUCCESS。


2. 消费(CONSUME)

  1. 用户下单支付,生成 wallet_transaction(type=CONSUME, status=INIT)。

  2. 开启事务:

    • 校验余额够不够。

    • 更新 wallet_account.balance = balance - amount。

    • 写入 wallet_ledger(change_amount = -amount)。

    • 更新 wallet_transaction.status=SUCCESS。


3. 冻结 & 解冻(如押金、待支付订单)

  • 冻结:balance - X,同时 frozen_balance + X,写流水。

  • 解冻:反向操作,写流水。

  • 消费冻结金额:frozen_balance - X,直接扣减,落账。


4. 退款(REFUND)

  1. 生成 wallet_transaction(type=REFUND, related_order_id=原消费单)。

  2. 开启事务:

    • balance = balance + refund_amount。

    • 写入 wallet_ledger。

    • 更新 wallet_transaction.status=SUCCESS。


📌 数据库逻辑

条件更新示例(不需要 version)

比如扣款:

UPDATE wallet_account 
SET balance = balance - 100 
WHERE user_id = 123 AND balance - frozen_balance >= 100;
  • 保证了账户可用余额足够再扣。

  • 如果余额不足,更新行数 = 0,表示失败,业务层处理异常。

  • 不存在并发覆盖的问题,因为每条语句都是 原子执行。


3. 冻结/解冻逻辑扩展

冻结金额需要同时更新 balance 和 frozen_balance,例如:

冻结资金(比如下单时锁定 200 元):

UPDATE wallet_account
SET balance = balance - 200,frozen_balance = frozen_balance + 200
WHERE user_id = 123AND balance - frozen_balance >= 200;

解冻资金(比如订单取消):

UPDATE wallet_account
SET balance = balance + 200,frozen_balance = frozen_balance - 200
WHERE user_id = 123AND frozen_balance >= 200;

消费冻结资金(比如订单支付成功):

UPDATE wallet_account
SET frozen_balance = frozen_balance - 200
WHERE user_id = 123AND frozen_balance >= 200;

 


📌 java代码实现(不使用version)

  1 import java.sql.*;
  2 import javax.sql.DataSource;
  3 
  4 public class WalletServiceConditional {
  5     private final DataSource ds;
  6     private final int MAX_RETRIES = 5;
  7 
  8     public WalletServiceConditional(DataSource ds) { this.ds = ds; }
  9 
 10     // 直接消费(不通过冻结)
 11     public boolean deductAvailable(long userId, long amount, String txnId) throws SQLException {
 12         // txnId 用于 ledger 的幂等约束
 13         for (int attempt=0; attempt<MAX_RETRIES; attempt++) {
 14             try (Connection conn = ds.getConnection()) {
 15                 conn.setAutoCommit(false);
 16                 try {
 17                     // 1) 原子更新余额:保证可用余额充足 (balance - frozen_balance >= amount)
 18                     String updSql = "UPDATE wallet_account " +
 19                                     "SET balance = balance - ? " +
 20                                     "WHERE user_id = ? AND (balance - frozen_balance) >= ?";
 21                     try (PreparedStatement upd = conn.prepareStatement(updSql)) {
 22                         upd.setLong(1, amount);
 23                         upd.setLong(2, userId);
 24                         upd.setLong(3, amount);
 25                         int affected = upd.executeUpdate();
 26                         if (affected == 0) {
 27                             conn.rollback();
 28                             return false; // 余额不足或其它原因
 29                         }
 30                     }
 31 
 32                     // 2) 写流水(注意幂等)
 33                     String insertLedger = "INSERT INTO wallet_ledger(txn_id, wallet_user_id, change_amount, balance_after, frozen_after, biz_type, created_at) " +
 34                                           "VALUES (?, ?, ?, (SELECT balance FROM wallet_account WHERE user_id=?), (SELECT frozen_balance FROM wallet_account WHERE user_id=?), ?, NOW())";
 35                     try (PreparedStatement ps = conn.prepareStatement(insertLedger)) {
 36                         ps.setString(1, txnId);
 37                         ps.setLong(2, userId);
 38                         ps.setLong(3, -amount);
 39                         ps.setLong(4, userId);
 40                         ps.setLong(5, userId);
 41                         ps.setString(6, "CONSUME");
 42                         ps.executeUpdate();
 43                     }
 44 
 45                     conn.commit();
 46                     return true;
 47                 } catch (SQLException ex) {
 48                     conn.rollback();
 49                     // 死锁重试:MySQL 错误码1213 或 SQLState "40001"
 50                     if (isDeadlock(ex) && attempt < MAX_RETRIES-1) {
 51                         backoffSleep(attempt);
 52                         continue;
 53                     }
 54                     throw ex;
 55                 } finally {
 56                     conn.setAutoCommit(true);
 57                 }
 58             }
 59         }
 60         throw new SQLException("deductAvailable failed after retries");
 61     }
 62 
 63     // 冻结资金(下单时)
 64     public boolean freezeAmount(long userId, long amount, String txnId) throws SQLException {
 65         for (int attempt=0; attempt<MAX_RETRIES; attempt++) {
 66             try (Connection conn = ds.getConnection()) {
 67                 conn.setAutoCommit(false);
 68                 try {
 69                     String sql = "UPDATE wallet_account SET balance = balance - ?, frozen_balance = frozen_balance + ? " +
 70                                  "WHERE user_id = ? AND balance >= ?";
 71                     try (PreparedStatement ps = conn.prepareStatement(sql)) {
 72                         ps.setLong(1, amount);
 73                         ps.setLong(2, amount);
 74                         ps.setLong(3, userId);
 75                         ps.setLong(4, amount);
 76                         int affected = ps.executeUpdate();
 77                         if (affected == 0) { conn.rollback(); return false; }
 78                     }
 79 
 80                     // ledger: freeze record
 81                     String lsql = "INSERT INTO wallet_ledger(txn_id, wallet_user_id, change_amount, balance_after, frozen_after, biz_type, created_at) " +
 82                                   "VALUES (?, ?, ?, (SELECT balance FROM wallet_account WHERE user_id=?), (SELECT frozen_balance FROM wallet_account WHERE user_id=?), ?, NOW())";
 83                     try (PreparedStatement ps2 = conn.prepareStatement(lsql)) {
 84                         ps2.setString(1, txnId);
 85                         ps2.setLong(2, userId);
 86                         ps2.setLong(3, -amount); // 把可用余额减少
 87                         ps2.setLong(4, userId);
 88                         ps2.setLong(5, userId);
 89                         ps2.setString(6, "FREEZE");
 90                         ps2.executeUpdate();
 91                     }
 92 
 93                     conn.commit();
 94                     return true;
 95                 } catch (SQLException ex) {
 96                     conn.rollback();
 97                     if (isDeadlock(ex) && attempt < MAX_RETRIES-1) {
 98                         backoffSleep(attempt);
 99                         continue;
100                     }
101                     throw ex;
102                 } finally {
103                     conn.setAutoCommit(true);
104                 }
105             }
106         }
107         throw new SQLException("freezeAmount failed after retries");
108     }
109 
110     private boolean isDeadlock(SQLException ex) {
111         return ex.getErrorCode() == 1213 || "40001".equals(ex.getSQLState());
112     }
113     private void backoffSleep(int attempt) {
114         try { Thread.sleep(50L * (attempt+1)); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
115     }
116 }

 


📌 防篡改与对账

  • hash_sign 字段:每次更新账户时,重新计算并写入。每天跑 一致性校验任务,比对签名是否正确。

  • 双录:流水和订单表都必须记录,且可相互对账。

  • 异地冷备:账务数据需要跨机房复制,防止单点损坏。

  • 定期对账:钱包系统内部对账(账户余额 = 所有流水累加),与支付通道对账(充值/提现)。


📌 钱包订单表的合并问题

  • 充值订单 和 消费订单 可以放在一个 wallet_transaction 表里(用 type 区分)。

  • 优点:查询用户交易历史简单。

  • 缺点:表会很大,可能需要分表(按 user_id/hash 分库分表)。

大厂(支付宝、微信、Paytm)一般也是用 一个订单表 + 一张流水表,而不是分成两张。

✅ 总结:

  • 余额、冻结金额 → 放在 wallet_account,原子更新。

  • 流水 → 必须单独表,保证账随流转。

  • 交易订单 → 一张表统一管理充值/消费/退款,配合状态机。

  • 防篡改 → 用 hash_sign 或日志落地校验。

  • 定期对账:钱包系统内部对账(账户余额 = 所有流水累加),与支付通道对账(充值/提现)。


在支付行业的钱包资金类场景里,更推荐使用 数据库事务 + 条件更新(悲观锁/行级锁/原子更新语句)来保障资金一致性,而不是依赖表里单独的 version 字段来做乐观锁。原因如下:

为什么事务比 version 更适合资金类场景

  • 资金安全 > 并发性能
    钱包、支付账户类场景,最重要的是 不能错扣、不能多扣。事务+原子条件更新(如 update ... set balance = balance - ? where balance >= ?)可以天然保证一致性。
    乐观锁(version)的设计适合“冲突少,读多写少”的业务,比如商品资料修改。
    但在资金类业务中,冲突概率高、金额精确要求极严,用 version 反而会导致:

    • 大量失败重试(因为 version 不匹配)

    • 并发下吞吐下降

    • 逻辑复杂(尤其冻结/解冻/退款等链路)

  • 数据库本身提供的 ACID 能力已经能保证数据正确性:

    • InnoDB 支持行级锁

    • 原子条件更新语句保证只有满足条件的数据能被修改

    • 事务回滚可以保障失败时不产生“脏扣款”

所以在资金类系统中,更常见的方式是依赖事务,不依赖 version。

 

相关新闻

  • 命令模式的深度解析:从标准实现到TPL Dataflow高性能架构
  • WF 2025 游记
  • 17.时间处理

最新新闻

  • 深入解析MCF5206嵌入式SoC:指令缓存与系统集成模块实战配置
  • 6/18
  • MPC555/556中断处理与代码压缩技术深度解析
  • 10分钟搞定黑苹果:OpCore-Simplify让OpenCore配置变得前所未有的简单
  • 显存不够用怎么办,vLLM 在 Instinct GPU 上的优化策略
  • 2026年全球高标准流体项目选型指南:主流自控阀门厂家技术盘点与多维工况实测 - 热点观察

日新闻

  • 5分钟掌握Python进化算法:Geatpy高性能优化工具完全指南
  • Microchip 24AA044 EEPROM选型与应用全指南:从参数解析到实战编程
  • 华为的鸿蒙到底有多牛?为什么称作遥遥领先?

周新闻

  • 3步解锁iOS设备:applera1n激活锁绕过完全指南
  • 39 2026 人工智能证书终极盘点,普通人选 AI 证书可以从这些方向入手
  • Redis 暴露公网有多危险?从端口检查到补救步骤

月新闻

  • 【总结】入门篇:50句话让你记住架构核心概念
  • WeChatMsg技术方案解析:实现Mac微信数据自主管理的完整解决方案
  • WeChatMsg:革新性微信数据备份方案,打造你的专属数字记忆库

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号