rwadurian/docs/issues/mining-contribution-sync-lo...

4.5 KiB
Raw Blame History

BUG: 挖矿分配并发覆盖贡献值同步 (Lost Update)

状态: 待解决 严重程度: 高 - 影响用户挖矿收益 发现日期: 2026-03-03 影响范围: 所有预种用户 (D2602260xxxx)12个账户数据异常


1. 问题描述

用户购买多份预种后contribution-service 正确记录了累计贡献值,但 mining-service 中只显示第一份的贡献值,导致挖矿收益严重偏低。

示例:

  • D26022600018 购买 4 份预种 → contribution 记录 13,117.860 → mining 只显示 3,279.465
  • D26022600001 购买 5 份合成 1 棵树 → contribution 记录 16,397.325 → mining 只显示 3,279.465

2. 根因分析

主因MiningDistributionService 的 save() 覆盖 totalContribution

mining-service 存在两个并发写入路径共享同一个 save() 方法:

路径 A - 贡献值同步Kafka 事件触发,不定时):

ContributionEventHandler → ContributionSyncService.handleContributionCalculated()
  → account.updateContribution(newValue)   // 更新 totalContribution
  → miningAccountRepository.save(account)  // upsert 所有字段

路径 B - 挖矿分配(每秒执行):

MiningDistributionService.executeSecondDistribution()
  → findAllWithContribution()              // 加载账户到内存(含 totalContribution
  → account.mine(reward)                   // 只改 totalMined/availableBalance
  → miningAccountRepository.save(account)  // upsert 所有字段(含 stale totalContribution

竞争时序:

T0  挖矿分配加载 D26022600018, totalContribution=3279.465
T1  贡献值同步更新 D26022600018 → totalContribution=13117.860 ✓ (DB 已更新)
T2  挖矿分配保存 D26022600018, 写回 totalContribution=3279.465 ← 覆盖!

日志证据:

03/01 02:50:30 - Updated contribution for D26022600001: 16397.325  ← 同步成功
03/03 当前 DB  - D26022600001 totalContribution = 3279.465         ← 被覆盖回旧值

挖矿分配每秒运行,贡献值同步后几乎必然在下一秒被覆盖。

代码位置:

  • mining-account.repository.ts:57-62 — save() 的 upsert 无条件写入所有字段
  • mining-distribution.service.ts:122-135 — 每秒加载→mine()→save()
  • mining-account.aggregate.ts:126-141 — mine() 只改余额但 save() 会写回所有字段

次因DailySnapshot 全量同步完全失效

本应作为安全网的每日全量同步,自上线以来一直 synced 0 accounts

03/01 01:00 - DailySnapshotCreated for 2026-02-27 → synced 0 accounts
03/02 01:00 - DailySnapshotCreated for 2026-02-28 → synced 0 accounts
03/03 01:00 - DailySnapshotCreated for 2026-03-01 → synced 0 accounts

原因:

  1. contribution-service 发布的事件 payload 缺少 snapshotId 字段 → mining-service 读到 undefined
  2. 字段名不匹配contribution 发 networkTotalContributionmining 读 totalContribution
  3. fetchContributionRatios() 返回数据解析失败(result.data 路径不对)

代码位置:

  • contribution-service/snapshot.service.ts:99-110 — payload 缺少 snapshotId
  • mining-service/contribution-sync.service.ts:63-68 — 期望 snapshotId
  • mining-service/contribution-sync.service.ts:126-141 — API 调用和解析

3. 受影响用户

用户 contribution 正确值 mining 当前值 差额
D26022600001 16,397.325 3,279.465 -13,117.860
D26022600014 13,253.562 3,279.465 -9,974.097
D26022600015 6,649.398 3,279.465 -3,369.933
D26022600016 13,117.860 3,279.465 -9,838.395
D26022600018 13,117.860 3,279.465 -9,838.395
D26022600033 3,754.422 3,279.465 -474.957
D26022600034 6,988.653 3,279.465 -3,709.188
D26022600035 3,799.656 3,392.550 -407.106
D26022600036 17,211.537 3,279.465 -13,932.072
D26022600037 16,397.325 3,279.465 -13,117.860
D26022600038 19,676.790 3,279.465 -16,397.325
D26022600043 6,558.930 3,279.465 -3,279.465

D26022600000 是唯一正常的预种用户。旧用户 (D2512xxxx) 不受影响。


4. 为什么旧用户不受影响

旧用户在系统稳定后完成了一次性同步,之后没有新的贡献值变更事件。 挖矿分配虽然每秒覆写 totalContribution但写回的值恰好等于正确值没有新同步来改变它

预种用户是分批多次产生贡献值的(先 1 份 → 2 份 → ... → N 份), 所以挖矿分配总是把后续累加的正确值覆盖回最初同步的旧值。