以太坊交易所钱包申请提币详解:从参数校验到数据库落库

·

关键词:以太坊交易所、ETH钱包开发、交易所钱包提币、交易所API 提币、Ethereum 提币接口、OutSerial幂等、地址校验、余额精度校验

在以太坊交易所钱包开发系列里,我们已经跑通了 ETH充值地址生成监听区块到账后台调度提币。接下来,用户在交易所 App 或 Web 页面中点击「提现」按钮后,平台需要做一次轻量、可靠的前置校验:申请提币。本文将逐行拆解这一过程,助你快速完成安全、幂等、可扩展的提币接口设计。


1. 请求入口与整体时序

提币的整体时序简化为三步:

  1. 前端携带参数向 /api/v1/withdraw/apply 发起 HTTP 请求;
  2. 服务端校验字段、去重、落库,返回流水号;
  3. 后台轮询任务从数据库取出 已审核 记录再去链上广播交易(见上一篇《提币调度》)。

👉 想立即参考完整源码?点我速通实战项目。

为了便于测试,我们仍使用基于 Gin 的中间件完成统一鉴权、统一响应封装,核心路径是 POST /api/v1/withdraw/apply


2. 请求参数结构体

Go 模型简写如下:

var req struct {
    Symbol   string `json:"symbol" binding:"required" validate:"oneof=eth"` // 仅支持 ETH
    OutSerial string `json:"out_serial" binding:"required" validate:"max=40"` // 外部业务流水
    Address   string `json:"address" binding:"required"`                      // 提币地址
    Balance   string `json:"balance" binding:"required"`                      // 提币数量,string 格式防丢精度
}
关键词:OutSerial、幂等、交易所提币接口

3. 地址格式与合法性校验

ETH 地址需要:

  1. 转小写:减少人为大小写混用导致的缓存、Key 重复。
  2. 正则校验:标准以太坊地址为 42 位十六进制字符串,前缀 0x。
req.Address = strings.ToLower(req.Address)
re := regexp.MustCompile(`^0x[0-9a-f]{40}$`)
if !re.MatchString(req.Address) {
    c.JSON(http.StatusOK, gin.H{
        "error": hcommon.ErrorAddressWrong,
        "err_msg": hcommon.ErrorAddressWrongMsg,
    })
    return
}

有两处细节常被忽略:


4. 提币金额精度校验

ETH 精度 = 10¹⁸ Wei,前端通常以 十进制字符串 方式传回,示例 "0.123"

使用 shopspring/decimal 包可防范以下三种错误:

代码片段:

balanceObj, err := decimal.NewFromString(req.Balance)
if err != nil || balanceObj.LessThanOrEqual(decimal.NewFromInt(0)) {
    c.JSON(http.StatusOK,
        gin.H{"error": hcommon.ErrorBalanceFormat, "err_msg": hcommon.ErrorBalanceFormatMsg})
    return
}
if balanceObj.Exponent() < -18 {
    c.JSON(http.StatusOK,
        gin.H{"error": hcommon.ErrorBalanceFormat, "err_msg": hcommon.ErrorBalanceFormatMsg})
    return
}
关键词:ETH 精度、decimal 库、交易所钱包余额精度

5. 幂等控制与数据库写入

一旦参数检验全部通过:

  1. 写入表 withdraw_apply(示例字段:out_serial/address/symbol/amount/status='pending'/created_at)。
  2. MySQL 给 out_serial唯一索引,同时把 address + amount + 时间戳做 MD5 token 作为日志冗余,便于追踪。
  3. 立即给出可读回执,让用户安心等待区块确认。
apply := model.WithdrawApply{
    OutSerial: req.OutSerial,
    Address:   req.Address,
    Symbol:    req.Symbol,
    Amount:    balanceObj.String(),
    Status:    StatusPending,
}
if err := db.Create(&apply).Error; err != nil {
    if mysqlErr, ok := err.(*mysql.MySQLError); ok && mysqlErr.Number == 1062 {
        c.JSON(http.StatusOK, gin.H{"error": "DUPLICATE_OUT_SERIAL"})
        return
    }
    hcommon.Log.Errorf("insert apply err: %v", err)
    c.JSON(http.StatusOK, gin.H{"error": "INTERNAL"})
    return
}

至此,申请阶段任务完成,后续由异步任务做 余额校验 -> 签交易 -> 广播 -> 回写哈希

👉 想一站式看完整提币流程代码?点此直达实现仓库。


6. 场景化示例:如何避免 UX 误区

场景:用户在高峰期,连续点击“提现”2 次,两次 OutSerial 相同。
后台在 50ms 内 完成写入后第二次返回 DUPLICATE_OUT_SERIAL,前端将按钮锁定并提示“已提交”,减少客服工单。

场景:运营要批量驳回异常提现单据,可在后台把状态从 pending 改成 reject,不会影响用户重新发起的 OutSerial。


7. 常见问题解答(FAQ)

Q1. OutSerial 一旦重复会发生什么?

A:数据库唯一键约束会直接拒绝,接口返回 DUPLICATE_OUT_SERIAL,建议前端随机 uuid 拼接用户 ID 确保全局唯一。

Q2. 为什么用 string 传余额?float64 不香吗?

A:JavaScript 的 0.1+0.2 !== 0.3,交易金额一旦发生 0.00000001 的偏差就可能导致用户投诉或账不平,string/decimal 可彻底避免。

Q3. 如何控制单笔/单日均提币限额?

A:申请阶段可先 读用户缓存(Redis 计数)做一次快速熔断,审核阶段再核对链上实际余额。遇大额度走人工审核流程。

Q4. 支持多链时,怎样扩展校验逻辑?

A:把地址正则、精度、最小提币额封装到 ChainConfig 结构体,按 symbol 从 map 读取即可,服务器零重启

Q5. 如何实时告诉用户“已发送但还未确认”?

A:后台广播完 tx 后把状态更新为 sent,同时把 tx hash 写回 apply 表;前端轮询或 websocket 推送到页面。


8. 小结

申请提币接口虽“轻”,却肩负安全检查、幂等控制与流量削峰三大任务:

合理拆分“申请”与“执行”既让用户体验流畅,也让开发者在后续升级支持 ERC-20、Layer2 或比特币时更游刃有余。