# 修复方案评估：挖矿分配并发覆盖贡献值

## 概览

需要解决三个问题：
1. **主因修复** — 消除挖矿分配对 totalContribution 的并发覆盖
2. **安全网修复** — 恢复 DailySnapshot 全量同步功能
3. **数据修复** — 将 12 个受影响用户的 mining 数据校正

---

## 方案对比

| 方案 | 改动范围 | 风险 | 可靠性 | 实施难度 |
|------|---------|------|-------|---------|
| A. 挖矿分配只更新挖矿字段 | 1个文件 | **低** | **高** | **低** |
| B. Repository 分方法 | 2个文件 | **低** | **高** | 低 |
| C. 数据库层面原子更新 | 1个文件 | 中 | 高 | 中 |
| D. 乐观锁 version 字段 | 4个文件+迁移 | 中高 | 高 | 高 |
| E. 贡献值独立表 | 多文件+迁移 | **高** | 高 | **高** |

---

## 方案 A：挖矿分配路径只更新挖矿字段（推荐）

**思路**：挖矿分配调用 save() 时，不写 totalContribution 和 lastSyncedAt。

**改动**：只改 `mining-account.repository.ts`，给 save() 加一个选项参数。

```typescript
// mining-account.repository.ts
async save(
  aggregate: MiningAccountAggregate,
  tx?: TransactionClient,
  options?: { skipContributionUpdate?: boolean },
): Promise<void> {
  const snapshot = aggregate.toSnapshot();

  const updateData: any = {
    totalMined: snapshot.totalMined.value,
    availableBalance: snapshot.availableBalance.value,
    frozenBalance: snapshot.frozenBalance.value,
  };

  // 仅在贡献值同步路径才写入这两个字段
  if (!options?.skipContributionUpdate) {
    updateData.totalContribution = snapshot.totalContribution.value;
    updateData.lastSyncedAt = snapshot.lastSyncedAt;
  }

  await client.miningAccount.upsert({
    where: { accountSequence: snapshot.accountSequence },
    create: { /* 所有字段 - 新账户需要全部 */ },
    update: updateData,
  });
}
```

**调用方改动**：只需在 mining-distribution.service.ts 的 save 调用加一个参数：
```typescript
await this.miningAccountRepository.save(account, tx, { skipContributionUpdate: true });
```

**优点**：
- 改动极小（1个文件加参数，1个调用方加选项）
- 向后兼容 — 不传 options 的调用方行为不变
- 完全消除竞争条件 — 挖矿分配路径不再触碰 totalContribution
- 不需要数据库迁移

**缺点**：
- 语义上 save() 变得不纯粹（不总是保存所有字段）
- 新增调用方需要知道传不传 options

**风险：低**
- 逻辑简单直观，不引入新概念
- mine() 本身就不修改 totalContribution，save() 不写它是逻辑自洽的
- 可通过单元测试充分验证

---

## 方案 B：拆分 Repository 方法

**思路**：创建专用的 `saveMiningResult()` 方法，只更新挖矿相关字段。

```typescript
// mining-account.repository.ts 新增方法
async saveMiningResult(aggregate: MiningAccountAggregate, tx?: TransactionClient): Promise<void> {
  const snapshot = aggregate.toSnapshot();
  const transactions = aggregate.pendingTransactions;

  const executeInTx = async (client: TransactionClient) => {
    await client.miningAccount.update({
      where: { accountSequence: snapshot.accountSequence },
      data: {
        totalMined: snapshot.totalMined.value,
        availableBalance: snapshot.availableBalance.value,
        frozenBalance: snapshot.frozenBalance.value,
        // 不写 totalContribution 和 lastSyncedAt
      },
    });

    if (transactions.length > 0) {
      await client.miningTransaction.createMany({ /* 同原逻辑 */ });
    }
  };
  // ...
}
```

**优点**：
- 方法职责清晰 — save() 保存全量，saveMiningResult() 只保存挖矿结果
- 不修改现有 save() 签名

**缺点**：
- 新增一个方法，代码量稍多
- 需要在 mining-distribution.service.ts 改调用方
- 用 update 而非 upsert，如果账户不存在会报错（但实际场景不会，因为有贡献值才会参与挖矿）

**风险：低**
- 与方案 A 本质相同，只是组织方式不同

---

## 方案 C：数据库层面原子 increment

**思路**：挖矿分配不加载整个 aggregate，直接用 Prisma 的 increment 操作。

```typescript
// mining-distribution.service.ts 中
await tx.miningAccount.update({
  where: { accountSequence: account.accountSequence },
  data: {
    totalMined: { increment: reward.value },
    availableBalance: { increment: reward.value },
    // 不写 totalContribution
  },
});
```

**优点**：
- 完全无竞争 — 数据库层面原子操作
- 不需要加载 aggregate

**缺点**：
- 绕过了 DDD aggregate 模式 — mine() 方法和 pendingTransactions 不再被使用
- 需要单独处理 MiningTransaction 的写入（不能从 aggregate 获取 balanceBefore/After）
- 改动较大，需要重构挖矿分配的核心逻辑
- 丢失了 aggregate 内的业务校验

**风险：中**
- 需要重构核心挖矿循环
- 需要额外逻辑计算 balanceBefore/After 用于交易流水
- 不再经过 aggregate 校验

---

## 方案 D：乐观锁 (version 字段)

**思路**：给 mining_accounts 加 version 字段，save 时检查版本号。

```prisma
model MiningAccount {
  // ... existing fields
  version Int @default(0)
}
```

```typescript
await client.miningAccount.update({
  where: { accountSequence: snapshot.accountSequence, version: snapshot.version },
  data: { ...fields, version: { increment: 1 } },
});
// 如果 version 不匹配，抛出异常，调用方重试
```

**优点**：
- 标准并发控制模式
- 适用于所有写入路径

**缺点**：
- 需要数据库迁移
- 需要在所有写入路径添加重试逻辑
- 挖矿分配每秒运行，重试频率可能很高
- 增加系统复杂度

**风险：中高**
- 数据库迁移需要在生产环境执行
- 重试逻辑引入额外复杂度
- 高频写入场景下乐观锁冲突率高，可能影响性能

---

## 方案 E：贡献值独立表

**思路**：将 totalContribution 从 mining_accounts 移出，或在查询时 JOIN contribution 表。

**不推荐** — 改动过大，涉及 schema 迁移、多处查询改动、跨服务依赖增加。

---

## DailySnapshot 安全网修复（独立于主因修复）

无论选择哪个主因方案，都需要修复 DailySnapshot 同步：

### 改动点 1：contribution-service 事件 payload 补全
```typescript
// snapshot.service.ts
payload: {
  snapshotDate: dateStr,
  snapshotId: snapshot.id?.toString(),     // 补充
  totalContribution: networkTotalContribution.toString(),  // 字段名对齐
  networkTotalContribution: networkTotalContribution.toString(),
  activeAccounts: activeAccounts.length,
  totalAccounts,
  createdAt: new Date().toISOString(),
},
```

### 改动点 2：mining-service 解析适配
```typescript
// contribution-sync.service.ts handleDailySnapshotCreated
const data = {
  snapshotId: data.snapshotId || 'unknown',
  snapshotDate: data.snapshotDate,
  totalContribution: data.totalContribution || data.networkTotalContribution || '0',
  activeAccounts: data.activeAccounts || data.accountCount || 0,
};
```

### 改动点 3：验证 API 端点
确认 `GET /api/v1/snapshots/{date}/ratios` 的返回格式与 `fetchContributionRatios()` 的解析匹配。

---

## 数据修复方案

主因修复部署后，需要一次性修正 12 个受影响用户的数据：

### 方式 1：触发全量 DailySnapshot 同步（推荐）
修好 DailySnapshot 后，手动触发一次全量同步，自动从 contribution-service 拉取所有正确值。

### 方式 2：手动 API 调用
对每个受影响用户，调用 contribution-service 获取 effectiveContribution，更新到 mining_accounts。

### 方式 3：直接 SQL 更新
从 contribution_accounts 表查出正确值，更新 mining_accounts（需要用户明确授权）。

---

## 推荐实施顺序

1. **方案 A**（主因修复）— 改动最小、风险最低、逻辑自洽
2. **DailySnapshot 修复** — 恢复安全网
3. **数据修复** — 部署后触发全量同步

三步均可在同一次部署中完成。
