Web3 Event Scan Starter
受众:开发者 摘要:区块扫描窗口、事件扫描编排、扫描器注册表、检查点持久化、Money Web3 自动扫描调度。
何时使用
| 场景 | 建议 |
|---|---|
| 计算下一段稳定扫描窗口 | Web3.nextScanWindow(...) |
| 事件扫描编排(读链头 + 查询日志 + 检查点) | Web3.scanEvents(...) |
注册自己的扫描器(按 scannerName) | Web3EventScannerRegistry |
| 扫描检查点 + 执行日志后台运维 | /api/v1/admin/web3/scan-checkpoints 等 |
| 事件幂等键 / 参数读取工具 | Web3EventIdempotencyKeys / Web3EventValues |
区块扫描窗口
生产环境做事件扫描时,通常不会直接扫描到最新链头,而是扣除确认数后只扫描稳定区块。Web3BlockScans 用于本地计算下一段扫描窗口,不访问 RPC、不落库、不启动任务:
Web3BlockScanWindow window = Web3BlockScans.nextWindow(
BigInteger.valueOf(18000000),
Web3.blockNumber("ethereum"),
12,
BigInteger.valueOf(1000));
if (window.isAvailable()) {
String fromBlock = window.getFromBlock().toString();
String toBlock = window.getToBlock().toString();
BigInteger nextBlock = window.getNextBlock();
}需要落库保存游标时,优先使用 Web3BlockScanCheckpoint 表达检查点。Money Web3 已经提供检查点和扫描日志的后台运维入口,业务模块也可以复用同一模型做自己的持久化:
Web3BlockScanCheckpoint checkpoint = Web3BlockScans.checkpoint(
"bsc",
"erc20-transfer",
BigInteger.valueOf(18000000));
Web3BlockScanWindow window = Web3BlockScans.nextWindow(
checkpoint,
Web3.blockNumber("bsc"),
12,
BigInteger.valueOf(1000));
if (window.isAvailable()) {
// 扫描 window.fromBlock -> window.toBlock。
checkpoint = Web3BlockScans.advance(checkpoint, window);
// 将 checkpoint.nextBlock 持久化,供下一轮扫描使用。
}约定:
nextBlock表示下一次待扫描起始区块,通常来自业务侧保存的游标。latestBlock表示当前链头区块,调用方可以通过Web3.blockNumber(chain)读取。requiredConfirmations用来扣除不稳定区块,例如链头120、确认数12,稳定区块为108。blockRangeSize用于限制单次扫描跨度;为null时一次扫描到稳定区块。- 当没有可扫描稳定区块时,
available=false,nextBlock保持不变。 advance(...)只应在本轮扫描成功后调用;扫描失败不要推进检查点。Web3BlockScanCheckpoint只提供通用模型,不负责落库、锁、重试或业务状态更新。
事件扫描编排
如果业务需要按检查点持续扫描事件,可以使用 Web3.scanEvents(...)。它会自动读取检查点对应链的当前区块高度,计算稳定扫描窗口,把窗口覆盖到 Web3EventLogQuery.fromBlock/toBlock,然后查询事件日志。
Web3EventScanResult result = Web3.scanEvents(Web3EventScanRequest.builder()
.checkpoint(checkpoint)
.query(Web3EventLogQuery.builder()
.address(contractAddress)
.event(transfer)
.build())
.requiredConfirmations(12)
.blockRangeSize(BigInteger.valueOf(1000))
.build(), context -> {
for (Web3EventLog log : context.getLogs()) {
// 在业务 Service 中做金额、地址、链 ID、事件唯一键和幂等校验。
}
});
if (result.isScanned()) {
Web3BlockScanCheckpoint next = result.getNextCheckpoint();
// 处理器正常返回后,再把 next.nextBlock 持久化。
}约定:
scanEvents(...)不落库、不加锁、不调度、不更新任何支付或资产业务状态。- 只有 handler 正常返回后,返回值里的
nextCheckpoint才代表本轮可以保存的游标。 - handler 抛出异常时,异常会向外传播,调用方不应推进数据库检查点。
- 无稳定区块可扫描时,
scanned=false,不会调用事件查询,也不会调用 handler。 - 多实例扫描、分布式锁、失败重试和任务调度应放在 Scheduler/Queue 或业务模块里完成。
事件扫描器注册表
如果多个业务模块都需要扫描链上事件,推荐实现 Web3EventScanner,由 Spring 自动注册到 Web3EventScannerRegistry。这样 Money Web3 只需要保存检查点上的 scannerName,真实业务查询条件和日志处理逻辑由各业务模块自己提供:
@Component
public class Erc20TransferScanner implements Web3EventScanner {
@Override
public String scannerName() {
return "erc20-transfer";
}
@Override
public Web3EventLogQuery query(Web3BlockScanCheckpoint checkpoint) {
return Web3EventLogQuery.builder()
.address("0x0000000000000000000000000000000000000002")
.event(Web3TokenEvents.ERC20_TRANSFER)
.build();
}
@Override
public void handle(Web3EventScanContext context) {
for (Web3EventLog log : context.getLogs()) {
String from = Web3EventValues.text(log, "from");
BigInteger value = Web3EventValues.bigInteger(log, "value");
// 在业务 Service 中做事件唯一键、金额、地址、链 ID 和幂等处理。
}
}
}调度任务或业务 Service 可以通过注册表取出扫描器,再复用 Web3.scanEvents(...):
Web3EventScanner scanner = scannerRegistry.getRequired(checkpoint.getScannerName());
Web3EventScanResult result = Web3.scanEvents(Web3EventScanRequest.builder()
.checkpoint(checkpoint)
.query(scanner.query(checkpoint))
.requiredConfirmations(12)
.blockRangeSize(BigInteger.valueOf(1000))
.build(), scanner::handle);约定:
scannerName使用小写短横线,例如erc20-transfer、web3-payment-confirmation。- 注册表按大小写不敏感方式匹配名称,并拒绝重复扫描器名。
- Web3 starter 只提供扫描器发现和基础编排,不直接落库、不加锁、不启动任务。
- 扫描任务的启停、分布式锁、失败重试、状态推进和业务幂等仍放在 Scheduler/Queue 或业务 Service 中。
仓库不再维护独立 Web3 样例模块。正式业务扫描器直接注册在业务模块中,例如 Money Web3 的余额充值扫描器会复用 Web3TokenEvents.ERC20_TRANSFER 构建查询,用 Web3EventIdempotencyKeys.contractEventLog(...) 生成业务去重键,并在业务 Service 事务内推进状态。
内置业务扫描器
Money Web3 模块会按业务需要注册少量正式扫描器。它们仍然复用 Web3 starter 的通用扫描编排,业务状态更新放在 Money Web3 Service 中完成。
| 扫描器 | 用途 | attributes |
|---|---|---|
user-balance-recharge-erc20-transfer | 匹配用户余额 Web3 充值的 ERC-20 Transfer 日志 | Token 合约地址,或 {"contractAddress":"0x..."} |
user-balance-recharge-erc20-transfer 只把链上转账事件绑定到待处理充值单:链、Token 合约、平台收款地址和金额全部一致时,写入付款地址、收款地址和交易哈希。它不直接入账、不归集资金、不签名、不广播;到账仍由 Web3 充值确认入口或自动确认任务按确认数推进。
扫描检查点管理
Web3 starter 提供 Web3BlockScanCheckpoint 和 Web3BlockScans 负责本地扫描窗口和游标推进规则;Money Web3 负责把游标落库并提供后台运维入口。
数据库表:
money_web3_scan_checkpoint
money_web3_scan_log后台 API:
GET /api/v1/admin/web3/scan-checkpoints
GET /api/v1/admin/web3/scan-checkpoints/{id}
GET /api/v1/admin/web3/scan-checkpoints/{id}/next-window
POST /api/v1/admin/web3/scan-checkpoints
PUT /api/v1/admin/web3/scan-checkpoints/{id}
POST /api/v1/admin/web3/scan-checkpoints/{id}/advance
POST /api/v1/admin/web3/scan-checkpoints/{id}/pause
POST /api/v1/admin/web3/scan-checkpoints/{id}/resume
POST /api/v1/admin/web3/scan-checkpoints/{id}/mark-error
DELETE /api/v1/admin/web3/scan-checkpoints/{id}
DELETE /api/v1/admin/web3/scan-checkpoints
POST /api/v1/admin/web3/scans/trigger
POST /api/v1/admin/web3/scans/checkpoints/{id}/trigger
GET /api/v1/admin/web3/scan-logs
GET /api/v1/admin/web3/scan-logs/{id}
DELETE /api/v1/admin/web3/scan-logs/{id}
DELETE /api/v1/admin/web3/scan-logs字段约定:
| 字段 | 说明 |
|---|---|
chainName | 链配置名称,例如 bsc、ethereum |
scannerName | 扫描器名称,例如 erc20-transfer、web3-payment-confirmation |
nextBlock | 下一次待扫描区块 |
lastScannedBlock | 上次扫描完成区块 |
latestBlock | 上次观察到的链头高度 |
stableBlock | 按确认数扣除后的稳定区块 |
requiredConfirmations | 扫描确认数 |
blockRangeSize | 单次扫描最大跨度 |
caughtUp | 是否已经追平稳定区块 |
status | active、paused、error |
next-window 会读取当前链头并预览下一次稳定扫描窗口,不触发真实扫描,也不推进游标。
advance 只推进检查点游标,不启动真实链扫描、不更新支付订单、不归档资产流水。pause / resume 用于扫描任务运行前判断是否跳过该检查点;mark-error 用于把失败原因留在检查点上,便于后台排查。
scans/trigger 是后台运维手动触发入口,会立即扫描 active 状态检查点,并返回本轮统计和每个检查点结果:
{
"limit": 10,
"traceId": "manual-20260520"
}如果只需要重试某个失败检查点,可以使用单检查点触发入口:
POST /api/v1/admin/web3/scans/checkpoints/{id}/trigger请求体只需要可选的 traceId。该入口会按指定检查点立即执行一次扫描,适合修复 RPC、扫描器或业务 handler 后对单个 error 检查点做精确重跑;它仍然复用同一套扫描日志、失败通知、游标推进和业务 handler 幂等规则。
扫描日志是系统生成的执行记录,只提供分页、详情、单条删除和批量删除,不提供人工创建或修改。自动扫描和手动触发扫描都会记录日志;日志不会影响扫描主流程,日志写入失败只记录应用日志,不会回滚已经成功的扫描和游标推进。
触发扫描结果的单个检查点项会返回 logCount 和 removedLogCount。其中 removedLogCount 表示本轮查询到的链上日志中,RPC 标记为 removed=true 的数量,通常用于提示疑似链重组或已回滚事件;业务扫描器仍应按自身幂等规则处理日志。
链上事件补偿任务的完整运行边界、幂等、失败处理和上线检查见 Web3 链上事件补偿任务。
扫描日志核心字段:
| 字段 | 说明 |
|---|---|
checkpointId | 对应检查点 ID |
chainName | 链配置名称 |
scannerName | 扫描器名称 |
status | success、skipped、failed |
scanned | 本轮是否实际扫描了稳定区块 |
logCount | 本轮查询到的链上日志数量 |
removedLogCount | 本轮查询到的 removed=true 日志数量,用于识别疑似链重组 |
fromBlock / toBlock | 本轮扫描区块范围 |
nextBlock | 本轮结束后建议保存的下一起始区块 |
latestBlock / stableBlock | 本轮观察到的链头和稳定区块 |
traceId | 手动触发或调度任务传入的追踪标识 |
startedAt / finishedAt | 本轮扫描开始和结束时间 |
durationMs | 本轮耗时,单位毫秒 |
errorMessage | 失败原因,最大保留 1000 字符 |
权限码:
money:web3:chain:view
money:web3:scan-checkpoint:view
money:web3:scan-checkpoint:create
money:web3:scan-checkpoint:update
money:web3:scan-checkpoint:delete
money:web3:scan:execute
money:web3:scan-log:view
money:web3:scan-log:deleteMoney Web3 内置一个轻量 Scheduler 扫描任务,默认每 1 分钟扫描一页 active 检查点。任务会根据检查点的 scannerName 从 Web3EventScannerRegistry 找到业务扫描器,调用 Web3.scanEvents(...),处理成功后推进游标并记录 success 或 skipped 执行日志;如果扫描器缺失、RPC 失败或业务 handler 抛错,会把检查点标记为 error,保存错误原因,并记录 failed 执行日志。
spring:
open:
money:
web3:
enabled: true
scanner:
enabled: true
auto-startup: true
task-name: money.web3.scan
instance-id: default
fixed-delay: 1m
limit: 50
failure-notification:
enabled: true
channels:
- log
subject: Web3 scan failed
recipient-type:
recipient-value:约定:
- 自动扫描只处理
active状态检查点;paused和error不会自动执行。 limit控制单轮最多扫描检查点数量,避免一次任务压垮 RPC 节点。- 自动扫描会按
lastScannedAt、id排序读取检查点,降低检查点数量较多时前几条反复占用扫描窗口的概率。 failure-notification复用 Notification starter,默认走log通道;如果改为database、mail、sms、webhook等通道,应同步配置 Notification 对应通道和接收人。- Scheduler Provider 负责任务级多实例协调;业务 handler 仍要做事件唯一键、金额、地址、链 ID 和本地状态幂等。
- Web3 starter 不直接写支付订单或资产流水,所有业务状态更新必须留在业务 Service 中。
业务 handler 做事件幂等时,优先使用 Web3EventIdempotencyKeys 生成统一去重键;读取事件字段时优先使用 Web3EventValues:
String key = Web3EventIdempotencyKeys.transactionLog("bsc", eventLog);
String scopedKey = Web3EventIdempotencyKeys.contractEventLog("bsc", eventLog);
String from = Web3EventValues.text(eventLog, "from");
BigInteger value = Web3EventValues.bigInteger(eventLog, "value");transactionLog 使用 chainName + transactionHash + logIndex,适合作为链上日志全局唯一键;contractEventLog 额外加入合约地址和事件签名,适合一个业务表同时归档多类合约事件。业务表应对该 key 建唯一索引,handler 成功落库后再允许推进检查点。
钱包登录流程本身仍由 IAM 管理,EVM 验签器由 Web3 starter 提供:
spring:
open:
web3:
wallet:
enabled: true
default-chain-type: evm
challenge-ttl: 5m
auto-create-user: true相关
- 事件日志查询 / topic 编码:web3-rpc.md
- Web3 支付链上确认 + 结算审计:web3-payment.md
- 链上事件补偿任务:web3-event-compensation.md
- Web3 顶层概览:web3.md