111 lines
4.5 KiB
Markdown
111 lines
4.5 KiB
Markdown
# 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 发 `networkTotalContribution`,mining 读 `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 份),
|
||
所以挖矿分配总是把后续累加的正确值覆盖回最初同步的旧值。
|