# 树转让功能详细实施方案

> 版本：v1.0 | 日期：2026-02-18 | 状态：待审批

---

## 一、概述与设计原则

### 1.1 功能目标

实现已认种树（状态为 `MINING_ENABLED`）在用户间的所有权转让，包括：
- 持仓变更（planting-service）
- 算力增量调整（contribution-service）
- 团队统计增减（referral-service）
- 资金结算（wallet-service）

### 1.2 核心设计原则

| # | 原则 | 说明 |
|---|------|------|
| 1 | **纯新增** | 各现有服务只新增事件消费者、方法、数据表，不修改已有业务逻辑 |
| 2 | **事务流水型** | 所有算力变动通过追加新流水记录实现，历史记录零修改、零删除 |
| 3 | **保持原始值** | 转让时沿用树的原始 `contributionPerTree`，不按转让日重算 |
| 4 | **历史不追回** | 已分配给卖方链路的历史算力记录保持不变，只从转让时刻起追加对冲/新增流水 |
| 5 | **可回滚** | Saga 编排每一步都有对应的补偿操作 |
| 6 | **奖励不重分** | reward-service 的历史分配记录（USDT 权益）不受转让影响 |
| 7 | **运营算力不动** | 仅运营账户 12% 为全局固定分配不受影响；省公司 1% 和市公司 2% 需随所有权变更调整 |
| 8 | **快照自动适配** | 挖矿快照每日按最新 effectiveContribution 全量生成，无需特殊处理 |

### 1.3 转让单位

- 转让以 **PlantingOrder（认种订单）** 为单位
- 支持整单转让（全部树）和部分转让（订单内指定棵数）
- 部分转让时，原订单不修改，通过 TransferRecord 追踪已转出的棵数

### 1.4 转让资格

- 卖方：树状态为 `MINING_ENABLED` 且未被锁定
- 买方：已完成实名认证的平台用户
- 同一棵树可多次转让（每次创建新的转让记录）

---

## 二、transfer-service 新微服务设计

### 2.1 服务概要

| 项目 | 值 |
|------|-----|
| 服务名 | transfer-service |
| 容器名 | rwa-transfer-service |
| 端口 | 3013 |
| Redis DB | 12 |
| 数据库 | rwa_transfer |
| 技术栈 | NestJS 10 + Prisma 5.7 + PostgreSQL 16 |
| Kafka Consumer Group | transfer-service-group |

### 2.2 数据模型

#### TransferOrder（转让订单 - Saga 聚合根）

```prisma
model TransferOrder {
  id                     BigInt    @id @default(autoincrement())
  transferOrderNo        String    @unique @map("transfer_order_no") @db.VarChar(50)

  // ========== 卖方信息 ==========
  sellerUserId           BigInt    @map("seller_user_id")
  sellerAccountSequence  String    @map("seller_account_sequence") @db.VarChar(20)

  // ========== 买方信息 ==========
  buyerUserId            BigInt    @map("buyer_user_id")
  buyerAccountSequence   String    @map("buyer_account_sequence") @db.VarChar(20)

  // ========== 转让标的 ==========
  sourceOrderNo          String    @map("source_order_no") @db.VarChar(50)      // 原认种订单号
  sourceAdoptionId       BigInt    @map("source_adoption_id")                    // 原认种ID
  treeCount              Int       @map("tree_count")                            // 转让棵数
  contributionPerTree    Decimal   @map("contribution_per_tree") @db.Decimal(20, 10) // 原始每棵算力值
  originalAdoptionDate   DateTime  @map("original_adoption_date") @db.Date       // 原始认种日期
  originalExpireDate     DateTime  @map("original_expire_date") @db.Date         // 原始算力到期日
  selectedProvince       String    @map("selected_province") @db.VarChar(10)     // 树所在省
  selectedCity           String    @map("selected_city") @db.VarChar(10)         // 树所在市

  // ========== 价格与费用 ==========
  transferPrice          Decimal   @map("transfer_price") @db.Decimal(20, 8)     // 转让总价（USDT）
  platformFeeRate        Decimal   @map("platform_fee_rate") @db.Decimal(5, 4)   // 平台手续费率
  platformFeeAmount      Decimal   @map("platform_fee_amount") @db.Decimal(20, 8) // 平台手续费
  sellerReceiveAmount    Decimal   @map("seller_receive_amount") @db.Decimal(20, 8) // 卖方实收

  // ========== Saga 状态 ==========
  status                 String    @default("PENDING") @map("status") @db.VarChar(30)
  sagaStep               String    @default("INIT") @map("saga_step") @db.VarChar(30)
  failReason             String?   @map("fail_reason") @db.VarChar(500)
  retryCount             Int       @default(0) @map("retry_count")

  // ========== 各步骤确认时间戳 ==========
  sellerConfirmedAt      DateTime? @map("seller_confirmed_at")
  paymentFrozenAt        DateTime? @map("payment_frozen_at")
  treesLockedAt          DateTime? @map("trees_locked_at")
  ownershipTransferredAt DateTime? @map("ownership_transferred_at")
  contributionAdjustedAt DateTime? @map("contribution_adjusted_at")
  statsUpdatedAt         DateTime? @map("stats_updated_at")
  paymentSettledAt       DateTime? @map("payment_settled_at")
  completedAt            DateTime? @map("completed_at")
  cancelledAt            DateTime? @map("cancelled_at")
  rolledBackAt           DateTime? @map("rolled_back_at")

  createdAt              DateTime  @default(now()) @map("created_at")
  updatedAt              DateTime  @updatedAt @map("updated_at")

  @@index([sellerUserId])
  @@index([buyerUserId])
  @@index([sourceOrderNo])
  @@index([status])
  @@index([createdAt])
  @@map("transfer_orders")
}
```

#### TransferStatusLog（状态变更日志 - 审计）

```prisma
model TransferStatusLog {
  id               BigInt   @id @default(autoincrement())
  transferOrderNo  String   @map("transfer_order_no") @db.VarChar(50)
  fromStatus       String   @map("from_status") @db.VarChar(30)
  toStatus         String   @map("to_status") @db.VarChar(30)
  fromSagaStep     String   @map("from_saga_step") @db.VarChar(30)
  toSagaStep       String   @map("to_saga_step") @db.VarChar(30)
  operatorType     String   @map("operator_type") @db.VarChar(20)   // USER / SYSTEM / ADMIN
  operatorId       String?  @map("operator_id") @db.VarChar(50)
  remark           String?  @db.VarChar(500)
  createdAt        DateTime @default(now()) @map("created_at")

  @@index([transferOrderNo])
  @@map("transfer_status_logs")
}
```

#### OutboxEvent（复用标准 Outbox 结构）

与其他服务完全相同的 Outbox 表结构。

### 2.3 Saga 状态机

#### TransferOrderStatus 枚举

```
PENDING              - 待卖方确认
SELLER_CONFIRMED     - 卖方已确认
PAYMENT_FROZEN       - 买方资金已冻结
TREES_LOCKED         - 卖方树已锁定
OWNERSHIP_TRANSFERRED - 所有权已变更
CONTRIBUTION_ADJUSTED - 算力已调整
STATS_UPDATED        - 团队统计已更新
PAYMENT_SETTLED      - 资金已结算
COMPLETED            - 转让完成

CANCELLED            - 已取消（PENDING/SELLER_CONFIRMED 阶段可取消）
FAILED               - 失败
ROLLING_BACK         - 补偿中
ROLLED_BACK          - 已回滚
```

#### SagaStep 枚举（编排步骤）

```
INIT                     - 初始化
FREEZE_BUYER_PAYMENT     - 冻结买方资金
LOCK_SELLER_TREES        - 锁定卖方树
TRANSFER_OWNERSHIP       - 变更所有权
ADJUST_CONTRIBUTION      - 调整算力
UPDATE_TEAM_STATS        - 更新团队统计
SETTLE_PAYMENT           - 结算资金
FINALIZE                 - 完成

COMPENSATE_UNLOCK_TREES  - 补偿：解锁树
COMPENSATE_UNFREEZE      - 补偿：解冻资金
```

#### 状态机流转图

```
用户发起转让
    ↓
PENDING (INIT)
    ↓ 卖方确认
SELLER_CONFIRMED (FREEZE_BUYER_PAYMENT)
    ↓ 冻结买方资金 → wallet-service
PAYMENT_FROZEN (LOCK_SELLER_TREES)
    ↓ 锁定卖方树 → planting-service
TREES_LOCKED (TRANSFER_OWNERSHIP)
    ↓ 变更所有权 → planting-service（发布事件）
OWNERSHIP_TRANSFERRED (ADJUST_CONTRIBUTION)
    ↓ 等待 contribution-service 确认（事件驱动）
CONTRIBUTION_ADJUSTED (UPDATE_TEAM_STATS)
    ↓ 等待 referral-service 确认（事件驱动）
STATS_UPDATED (SETTLE_PAYMENT)
    ↓ 结算资金 → wallet-service
PAYMENT_SETTLED (FINALIZE)
    ↓
COMPLETED

失败补偿路径（任何步骤失败）：
  FAILED → ROLLING_BACK
    → COMPENSATE_UNLOCK_TREES（如果树已锁定）
    → COMPENSATE_UNFREEZE（如果资金已冻结）
    → ROLLED_BACK
```

### 2.4 API 设计

#### 用户端 API

```
POST   /transfers                    - 发起转让（买方向卖方购买）
POST   /transfers/:id/seller-confirm - 卖方确认
POST   /transfers/:id/cancel         - 取消转让
GET    /transfers                    - 查询我的转让记录（买/卖）
GET    /transfers/:id                - 转让详情
```

#### 管理端 API

```
GET    /admin/transfers              - 转让列表（筛选、分页）
GET    /admin/transfers/:id          - 转让详情
GET    /admin/transfers/stats        - 转让统计
POST   /admin/transfers/:id/force-cancel - 强制取消
```

### 2.5 转让号生成规则

```
TRF + YYMMDD + 随机字符串（8位）
示例：TRF260218A3B7C9D1
```

---

## 三、planting-service 纯新增设计

### 3.1 新增数据表

#### PlantingTransferRecord（转让记录）

```prisma
model PlantingTransferRecord {
  id                     BigInt    @id @default(autoincrement())
  transferOrderNo        String    @unique @map("transfer_order_no") @db.VarChar(50)

  // ========== 原订单信息 ==========
  sourceOrderNo          String    @map("source_order_no") @db.VarChar(50)
  sourceOrderId          BigInt    @map("source_order_id")

  // ========== 卖方 ==========
  fromUserId             BigInt    @map("from_user_id")
  fromAccountSequence    String    @map("from_account_sequence") @db.VarChar(20)

  // ========== 买方 ==========
  toUserId               BigInt    @map("to_user_id")
  toAccountSequence      String    @map("to_account_sequence") @db.VarChar(20)

  // ========== 转让内容 ==========
  treeCount              Int       @map("tree_count")
  selectedProvince       String    @map("selected_province") @db.VarChar(10)
  selectedCity           String    @map("selected_city") @db.VarChar(10)
  contributionPerTree    Decimal   @map("contribution_per_tree") @db.Decimal(20, 10)
  originalAdoptionDate   DateTime  @map("original_adoption_date") @db.Date
  originalExpireDate     DateTime  @map("original_expire_date") @db.Date

  // ========== 状态 ==========
  status                 String    @default("LOCKED") @map("status") @db.VarChar(20)
  // LOCKED → TRANSFERRED → ROLLED_BACK

  lockedAt               DateTime? @map("locked_at")
  transferredAt          DateTime? @map("transferred_at")
  rolledBackAt           DateTime? @map("rolled_back_at")
  createdAt              DateTime  @default(now()) @map("created_at")

  @@index([sourceOrderNo])
  @@index([fromUserId])
  @@index([toUserId])
  @@index([status])
  @@map("planting_transfer_records")
}
```

### 3.2 PlantingOrder 新增字段

在 PlantingOrder 表上新增一个字段用于标记转让锁定状态（通过 Prisma migration 新增列）：

```prisma
// 新增字段（可空，不影响现有数据）
transferLockedCount    Int       @default(0) @map("transfer_locked_count")  // 被锁定的转让棵数
```

**可转让棵数** = `treeCount - transferLockedCount - 已完成转让的棵数`

已完成转让的棵数通过查询 `PlantingTransferRecord` 表（status = TRANSFERRED）聚合得到。

### 3.3 新增事件消费处理器

#### transfer-lock.handler.ts（锁定树）

- **消费 Topic**：`transfer.trees.lock`
- **处理逻辑**：
  1. 验证卖方持有该订单且有足够可转让棵数
  2. 创建 `PlantingTransferRecord`（status = LOCKED）
  3. 更新 `PlantingOrder.transferLockedCount += treeCount`
  4. 发布确认事件到 `transfer.trees.lock.ack`

#### transfer-execute.handler.ts（执行所有权变更）

- **消费 Topic**：`transfer.ownership.execute`
- **处理逻辑**：
  1. 查找对应的 `PlantingTransferRecord`
  2. 更新 `PlantingTransferRecord.status = TRANSFERRED`
  3. 更新卖方 `PlantingPosition`：
     - `effectiveTreeCount -= treeCount`
     - `totalTreeCount -= treeCount`
  4. 更新卖方 `PositionDistribution`（对应省市的持仓数 -= treeCount）
  5. 更新/创建买方 `PlantingPosition`：
     - `effectiveTreeCount += treeCount`
     - `totalTreeCount += treeCount`
  6. 更新/创建买方 `PositionDistribution`（对应省市的持仓数 += treeCount）
  7. 发布 Outbox 事件（3 个）：
     - `planting.transfer.completed`（通知 transfer-service）
     - `planting.ownership.removed`（通知 contribution + referral）
     - `planting.ownership.added`（通知 contribution + referral）

#### transfer-rollback.handler.ts（回滚锁定）

- **消费 Topic**：`transfer.trees.unlock`
- **处理逻辑**：
  1. 查找对应的 `PlantingTransferRecord`
  2. 更新 `PlantingTransferRecord.status = ROLLED_BACK`
  3. 更新 `PlantingOrder.transferLockedCount -= treeCount`
  4. 发布确认事件到 `transfer.trees.unlock.ack`

### 3.4 新增 Outbox 事件

| 事件 | Topic | AggregateType | Payload |
|------|-------|---------------|---------|
| `PlantingTransferLocked` | `planting.transfer.locked` | `PlantingTransfer` | transferOrderNo, sourceOrderNo, treeCount |
| `PlantingTransferCompleted` | `planting.transfer.completed` | `PlantingTransfer` | transferOrderNo, 完整卖方/买方信息 |
| `PlantingOwnershipRemoved` | `planting.ownership.removed` | `PlantingTransfer` | 卖方信息 + 树信息 |
| `PlantingOwnershipAdded` | `planting.ownership.added` | `PlantingTransfer` | 买方信息 + 树信息 |
| `PlantingTransferRolledBack` | `planting.transfer.rolledback` | `PlantingTransfer` | transferOrderNo |

---

## 四、referral-service 纯新增设计

### 4.1 新增事件处理器

#### planting-transferred.handler.ts

- **消费 Topic**：`planting.ownership.removed` + `planting.ownership.added`
- **Consumer Group**：`referral-service-transfer`

### 4.2 处理 planting.ownership.removed（卖方减少）

```typescript
async handleOwnershipRemoved(event: PlantingOwnershipRemovedEvent): Promise<void> {
  // 1. 更新卖方个人认种统计
  const sellerStats = await this.teamStatsRepo.findByUserId(event.sellerUserId);
  sellerStats.removePersonalPlanting(event.treeCount, event.provinceCode, event.cityCode);
  //   selfPlantingCount -= treeCount
  //   selfPlantingAmount -= treeCount * pricePerTree
  //   provinceCityDistribution 对应省市 -= treeCount
  await this.teamStatsRepo.save(sellerStats);

  // 2. 获取卖方的上级链
  const relationship = await this.referralRepo.findByUserId(event.sellerUserId);
  const ancestors = relationship.getAllAncestorIds();

  // 3. 批量更新上级团队统计（负数 delta）
  const updates = ancestors.map((ancestorId, i) => ({
    userId: ancestorId,
    countDelta: -event.treeCount,          // 关键：负数
    provinceCode: event.provinceCode,
    cityCode: event.cityCode,
    fromDirectReferralId: i === 0 ? event.sellerUserId : ancestors[0],
  }));

  await this.teamStatsRepo.batchUpdateTeamCountsForTransfer(updates);
  //   totalTeamPlantingCount -= treeCount
  //   DirectReferral.teamPlantingCount -= treeCount（increment 负数）
  //   重新扫描所有直推的 teamPlantingCount 找新 max（关键修正）
  //   effectivePlantingCountForRanking = totalTeamPlantingCount - newMax
}
```

### 4.3 处理 planting.ownership.added（买方增加）

```typescript
async handleOwnershipAdded(event: PlantingOwnershipAddedEvent): Promise<void> {
  // 1. 更新买方个人认种统计
  const buyerStats = await this.teamStatsRepo.findByUserId(event.buyerUserId);
  buyerStats.addPersonalPlanting(event.treeCount, event.provinceCode, event.cityCode);
  await this.teamStatsRepo.save(buyerStats);

  // 2. 获取买方的上级链
  const relationship = await this.referralRepo.findByUserId(event.buyerUserId);
  const ancestors = relationship.getAllAncestorIds();

  // 3. 批量更新上级团队统计（正数 delta，复用现有 batchUpdateTeamCounts）
  const updates = ancestors.map((ancestorId, i) => ({
    userId: ancestorId,
    countDelta: event.treeCount,           // 正数
    provinceCode: event.provinceCode,
    cityCode: event.cityCode,
    fromDirectReferralId: i === 0 ? event.buyerUserId : ancestors[0],
  }));

  await this.teamStatsRepo.batchUpdateTeamCounts(updates);  // 复用现有方法
}
```

### 4.4 新增方法

#### TeamStatistics.removePersonalPlanting()（聚合根新增方法）

```typescript
removePersonalPlanting(count: number, provinceCode: string, cityCode: string): void {
  this._selfPlantingCount = Math.max(0, this._selfPlantingCount - count);
  this._selfPlantingAmount = this._selfPlantingAmount.minus(count * PRICE_PER_TREE);

  // 更新省市分布
  const dist = this._provinceCityDistribution;
  if (dist[provinceCode]?.[cityCode]) {
    dist[provinceCode][cityCode] = Math.max(0, dist[provinceCode][cityCode] - count);
  }

  this._updatedAt = new Date();
}
```

#### TeamStatisticsRepository.batchUpdateTeamCountsForTransfer()（新增方法）

与现有 `batchUpdateTeamCounts` 逻辑类似，但在处理负数 delta 时：

```typescript
// 关键差异：负数 delta 时重新扫描所有直推找 max
if (update.countDelta < 0) {
  // 更新直推的 teamPlantingCount（允许负数 increment）
  await tx.directReferral.updateMany({
    where: { referrerId: update.userId, referralId: update.fromDirectReferralId },
    data: { teamPlantingCount: { increment: update.countDelta } },
  });

  // 重新扫描所有直推的 teamPlantingCount 找最大值
  const allDirectReferrals = await tx.directReferral.findMany({
    where: { referrerId: update.userId },
    select: { teamPlantingCount: true },
  });
  const newMax = Math.max(0, ...allDirectReferrals.map(r => r.teamPlantingCount));

  const newTotal = currentStats.totalTeamPlantingCount + update.countDelta;
  const newEffective = Math.max(0, newTotal - newMax);

  await tx.teamStatistics.update({
    where: { userId: update.userId },
    data: {
      totalTeamPlantingCount: Math.max(0, newTotal),
      maxSingleTeamPlantingCount: newMax,
      effectivePlantingCountForRanking: newEffective,
    },
  });
}
```

### 4.5 发布确认事件

处理完成后发布事件到 `referral.transfer.stats-updated`，供 transfer-service 确认 Saga 步骤。

---

## 五、contribution-service 纯新增设计（核心）

### 5.1 算力调整原则

```
原则一：历史记录不修改、不删除
原则二：所有调整通过追加新流水记录实现（对冲式）
原则三：保持原始 contributionPerTree，沿用原认种参数
原则四：新记录的 expireDate = 原始认种的 expireDate（剩余有效期）
原则五：解锁状态需要更新（影响未来新认种的分配，但不追回历史）
原则六：调整 88%（个人70% + 团队层级7.5% + 个人加成7.5% + 省1% + 市2%），仅运营12% 不动
原则七：挖矿快照每日按最新 effectiveContribution 自动生成，无需干预
```

### 5.1.1 算力影响范围明确界定

| 算力类别 | 比例 | 分配对象 | 转让时需调整？ | 原因 |
|---------|------|---------|:---:|------|
| 个人算力 | 70% | 认种人 | **是** | 所有权转移，算力跟着树走 |
| 团队层级 | 7.5% | 认种人 15 级上线 | **是** | 上线链从卖方链路换成买方链路 |
| 个人加成 | 7.5% | 认种人（T1/T2/T3） | **是** | 所有权转移，加成跟着树走 |
| 省公司 | 1% | 按树所在省的系统账户 | **是** | 所有权变更，省公司授权体系随新所有者调整 |
| 市公司 | 2% | 按树所在市的系统账户 | **是** | 所有权变更，市公司授权体系随新所有者调整 |
| 运营账户 | 12% | OPERATION 系统账户 | **否** | 全局固定分配，与用户和地域无关 |

**结论**：转让需处理 88% 的算力调整，仅运营账户 12% 完全不受影响。

### 5.2 新增 sourceType 枚举值

在现有 ContributionRecord 的 sourceType 基础上新增：

```
// 转让转出（卖方侧，金额为负）
TRANSFER_OUT_PERSONAL        - 卖方个人算力转出
TRANSFER_OUT_TEAM_LEVEL      - 卖方上线团队层级算力转出
TRANSFER_OUT_BONUS           - 卖方个人加成算力转出

// 转让转入（买方侧，金额为正）
TRANSFER_IN_PERSONAL         - 买方个人算力转入
TRANSFER_IN_TEAM_LEVEL       - 买方上线团队层级算力转入
TRANSFER_IN_BONUS            - 买方个人加成算力转入

// 系统账户调整（省公司 1% + 市公司 2%，随所有权变更）
TRANSFER_OUT_SYSTEM_PROVINCE - 卖方省公司系统账户算力转出
TRANSFER_OUT_SYSTEM_CITY     - 卖方市公司系统账户算力转出
TRANSFER_IN_SYSTEM_PROVINCE  - 买方省公司系统账户算力转入
TRANSFER_IN_SYSTEM_CITY      - 买方市公司系统账户算力转入

// 注意：仅运营账户 12% 为全局固定分配，不受转让影响
```

### 5.3 ContributionRecord 新增字段

```prisma
// 新增可空字段（不影响现有数据）
transferOrderNo    String? @map("transfer_order_no") @db.VarChar(50)  // 关联转让订单号
```

### 5.4 卖方算力扣减流水（planting.ownership.removed 事件处理）

#### 5.4.1 个人算力扣减

```typescript
// 创建一条负数流水
ContributionRecord.create({
  accountSequence: seller.accountSequence,
  sourceType: 'TRANSFER_OUT_PERSONAL',
  sourceAdoptionId: originalAdoptionId,
  sourceAccountSequence: seller.accountSequence,
  treeCount: transferTreeCount,
  baseContribution: originalContributionPerTree,
  distributionRate: 0.70,
  amount: -(originalContributionPerTree * transferTreeCount * 0.70),  // 负数
  transferOrderNo: transferOrderNo,
  effectiveDate: transferDate,           // 转让日期次日
  expireDate: originalExpireDate,        // 原始到期日
  status: 'EFFECTIVE',
  remark: `转让转出至 ${buyer.accountSequence}`,
});

// 更新卖方 ContributionAccount
sellerAccount.personalContribution -= amount;
sellerAccount.effectiveContribution -= amount;
```

#### 5.4.2 卖方上线团队层级扣减

对卖方的 15 级上线，每级创建一条负数流水：

```typescript
for (let level = 1; level <= 15; level++) {
  const ancestor = sellerAncestorChain[level - 1];
  if (!ancestor) break;

  const levelAmount = originalContributionPerTree * transferTreeCount * 0.005;

  // 检查当时这一级是否已分配（查询原始 ContributionRecord）
  const originalRecord = await findOriginalTeamLevelRecord(
    ancestor.accountSequence, originalAdoptionId, level
  );

  if (originalRecord && originalRecord.status === 'EFFECTIVE') {
    // 原始记录是已生效的 → 创建负数对冲流水
    ContributionRecord.create({
      accountSequence: ancestor.accountSequence,
      sourceType: 'TRANSFER_OUT_TEAM_LEVEL',
      sourceAdoptionId: originalAdoptionId,
      sourceAccountSequence: seller.accountSequence,
      treeCount: transferTreeCount,
      baseContribution: originalContributionPerTree,
      distributionRate: 0.005,
      levelDepth: level,
      amount: -levelAmount,
      transferOrderNo: transferOrderNo,
      effectiveDate: transferDate,
      expireDate: originalExpireDate,
      status: 'EFFECTIVE',
      remark: `转让转出：${seller.accountSequence} → ${buyer.accountSequence}`,
    });

    // 更新上线 ContributionAccount
    ancestorAccount.effectiveContribution -= levelAmount;
    // 对应层级的 pending/unlocked 字段也需调整
  }
  // 如果原始记录在 UnallocatedContribution 中（上线未解锁），
  // 则不需要扣减（因为从未分配给该上线），但需标记该 Unallocated 记录已失效
}
```

#### 5.4.3 卖方加成扣减

对卖方自身已生效的加成档位，创建负数流水：

```typescript
for (let tier = 1; tier <= 3; tier++) {
  // 查询卖方在原始认种中该档位是否已获得
  const originalBonusRecord = await findOriginalBonusRecord(
    seller.accountSequence, originalAdoptionId, tier
  );

  if (originalBonusRecord && originalBonusRecord.status === 'EFFECTIVE') {
    const bonusAmount = originalContributionPerTree * transferTreeCount * 0.025;

    ContributionRecord.create({
      accountSequence: seller.accountSequence,
      sourceType: 'TRANSFER_OUT_BONUS',
      sourceAdoptionId: originalAdoptionId,
      sourceAccountSequence: seller.accountSequence,
      treeCount: transferTreeCount,
      baseContribution: originalContributionPerTree,
      distributionRate: 0.025,
      bonusTier: tier,
      amount: -bonusAmount,
      transferOrderNo: transferOrderNo,
      effectiveDate: transferDate,
      expireDate: originalExpireDate,
      status: 'EFFECTIVE',
      remark: `转让转出加成T${tier}`,
    });

    sellerAccount.effectiveContribution -= bonusAmount;
  }
}
```

#### 5.4.4 卖方解锁状态更新

```typescript
// 重新计算卖方的 selfPlantingCount
const remainingAdoptions = await syncedDataRepository.countAdoptionsByAccount(
  seller.accountSequence, excludeTransferred: true
);

if (remainingAdoptions === 0) {
  // 卖方不再持有任何树
  sellerAccount.hasAdopted = false;
  sellerAccount.unlockedLevelDepth = 0;
  // unlockedBonusTiers 的 T1 失效，但 T2/T3 取决于 directReferralAdoptedCount
  sellerAccount.recalculateUnlockStatus();
}

// 检查卖方的推荐人的 directReferralAdoptedCount 是否需要更新
if (remainingAdoptions === 0) {
  const sellerReferrer = await findReferrer(seller.accountSequence);
  if (sellerReferrer) {
    const newCount = await recalculateDirectReferralAdoptedCount(sellerReferrer.accountSequence);
    if (newCount < sellerReferrer.directReferralAdoptedCount) {
      sellerReferrerAccount.directReferralAdoptedCount = newCount;
      sellerReferrerAccount.recalculateUnlockStatus();
      // 注意：不追回历史分配，只更新状态（影响未来分配）
    }
  }
}
```

### 5.5 买方算力新增流水（planting.ownership.added 事件处理）

#### 5.5.1 买方个人算力

```typescript
ContributionRecord.create({
  accountSequence: buyer.accountSequence,
  sourceType: 'TRANSFER_IN_PERSONAL',
  sourceAdoptionId: originalAdoptionId,
  sourceAccountSequence: buyer.accountSequence,  // 新的所有者
  treeCount: transferTreeCount,
  baseContribution: originalContributionPerTree,  // 保持原始值
  distributionRate: 0.70,
  amount: originalContributionPerTree * transferTreeCount * 0.70,  // 正数
  transferOrderNo: transferOrderNo,
  effectiveDate: transferDate,
  expireDate: originalExpireDate,
  status: 'EFFECTIVE',
  remark: `转让转入来自 ${seller.accountSequence}`,
});

buyerAccount.personalContribution += amount;
buyerAccount.effectiveContribution += amount;
```

#### 5.5.2 买方上线团队层级分配

```typescript
for (let level = 1; level <= 15; level++) {
  const ancestor = buyerAncestorChain[level - 1];
  if (!ancestor) {
    // 无上线 → UnallocatedContribution（归总部）
    createUnallocatedContribution({
      sourceAdoptionId: originalAdoptionId,
      sourceAccountSequence: buyer.accountSequence,
      unallocType: 'TRANSFER_IN_LEVEL_NO_ANCESTOR',
      levelDepth: level,
      amount: levelAmount,
      transferOrderNo: transferOrderNo,
    });
    continue;
  }

  const ancestorAccount = await findContributionAccount(ancestor.accountSequence);

  if (ancestorAccount && ancestorAccount.unlockedLevelDepth >= level) {
    // 上线已解锁 → 直接分配
    ContributionRecord.create({
      accountSequence: ancestor.accountSequence,
      sourceType: 'TRANSFER_IN_TEAM_LEVEL',
      sourceAdoptionId: originalAdoptionId,
      sourceAccountSequence: buyer.accountSequence,
      levelDepth: level,
      amount: levelAmount,  // 正数
      transferOrderNo: transferOrderNo,
      effectiveDate: transferDate,
      expireDate: originalExpireDate,
      status: 'EFFECTIVE',
    });
    ancestorAccount.effectiveContribution += levelAmount;
  } else {
    // 上线未解锁 → UnallocatedContribution（待补发）
    createUnallocatedContribution({
      sourceAdoptionId: originalAdoptionId,
      sourceAccountSequence: buyer.accountSequence,
      unallocType: 'LEVEL_OVERFLOW',
      wouldBeAccountSequence: ancestor.accountSequence,
      levelDepth: level,
      amount: levelAmount,
      transferOrderNo: transferOrderNo,
    });
  }
}
```

#### 5.5.3 买方加成分配

```typescript
const buyerUnlockedBonusTiers = calculateUnlockedBonusTiers(
  buyerAccount.hasAdopted || true,  // 买方获得树后 hasAdopted = true
  buyerAccount.directReferralAdoptedCount
);

for (let tier = 1; tier <= 3; tier++) {
  if (buyerUnlockedBonusTiers >= tier) {
    ContributionRecord.create({
      accountSequence: buyer.accountSequence,
      sourceType: 'TRANSFER_IN_BONUS',
      bonusTier: tier,
      amount: bonusAmount,
      transferOrderNo: transferOrderNo,
      // ...
    });
    buyerAccount.effectiveContribution += bonusAmount;
  } else {
    createUnallocatedContribution({
      unallocType: `BONUS_TIER_${tier}_PENDING`,
      wouldBeAccountSequence: buyer.accountSequence,
      amount: bonusAmount,
      transferOrderNo: transferOrderNo,
    });
  }
}
```

#### 5.5.4 买方解锁状态更新

```typescript
// 更新买方 hasAdopted
if (!buyerAccount.hasAdopted) {
  buyerAccount.markAsAdopted();  // hasAdopted = true, unlockedLevelDepth = 5, unlockedBonusTiers = 1
}

// 检查买方的推荐人的 directReferralAdoptedCount 是否需要更新
const buyerReferrer = await findReferrer(buyer.accountSequence);
if (buyerReferrer) {
  const newCount = await recalculateDirectReferralAdoptedCount(buyerReferrer.accountSequence);
  if (newCount > buyerReferrer.directReferralAdoptedCount) {
    // 可能触发 checkAndClaimBonus()（复用现有逻辑）
    await bonusClaimService.checkAndClaimBonus(
      buyerReferrer.accountSequence,
      buyerReferrer.directReferralAdoptedCount,
      newCount
    );
    buyerReferrerAccount.directReferralAdoptedCount = newCount;
    buyerReferrerAccount.recalculateUnlockStatus();
  }
}
```

### 5.6 系统账户处理

**运营账户 12% 不受影响**（全局固定分配）。

**省公司 1% 和市公司 2% 需随所有权变更调整**：

```typescript
// 卖方省市系统账户：创建负数对冲流水
const provinceAmount = originalContributionPerTree * transferTreeCount * 0.01;
const cityAmount = originalContributionPerTree * transferTreeCount * 0.02;

// 卖方省公司系统账户扣减
SystemContributionRecord.create({
  accountType: 'PROVINCE',
  regionCode: sellerSelectedProvince,   // 原始认种时选择的省
  sourceType: 'TRANSFER_OUT_SYSTEM_PROVINCE',
  sourceAdoptionId: originalAdoptionId,
  amount: -provinceAmount,              // 负数
  transferOrderNo: transferOrderNo,
  effectiveDate: transferDate,
  expireDate: originalExpireDate,
});
// 更新 SystemAccount (PROVINCE, sellerSelectedProvince) 的 contributionBalance

// 卖方市公司系统账户扣减
SystemContributionRecord.create({
  accountType: 'CITY',
  regionCode: sellerSelectedCity,       // 原始认种时选择的市
  sourceType: 'TRANSFER_OUT_SYSTEM_CITY',
  sourceAdoptionId: originalAdoptionId,
  amount: -cityAmount,
  transferOrderNo: transferOrderNo,
  effectiveDate: transferDate,
  expireDate: originalExpireDate,
});

// 买方省公司系统账户增加
SystemContributionRecord.create({
  accountType: 'PROVINCE',
  regionCode: buyerSelectedProvince,    // 买方的省（来自 PlantingOwnershipAdded 事件）
  sourceType: 'TRANSFER_IN_SYSTEM_PROVINCE',
  sourceAdoptionId: originalAdoptionId,
  amount: +provinceAmount,
  transferOrderNo: transferOrderNo,
  effectiveDate: transferDate,
  expireDate: originalExpireDate,
});

// 买方市公司系统账户增加
SystemContributionRecord.create({
  accountType: 'CITY',
  regionCode: buyerSelectedCity,
  sourceType: 'TRANSFER_IN_SYSTEM_CITY',
  sourceAdoptionId: originalAdoptionId,
  amount: +cityAmount,
  transferOrderNo: transferOrderNo,
  effectiveDate: transferDate,
  expireDate: originalExpireDate,
});
```

**注意**：如果卖方和买方在同一省市，省市系统账户的对冲流水互相抵消，净变化为零，但流水记录仍然要写（审计需要）。

### 5.7 挖矿快照自动适配

**挖矿分配同样无需特殊处理**：

1. 转让完成后，受影响用户的 `ContributionAccount.effectiveContribution` 已实时更新
2. 每日定时任务 `SnapshotService.createDailySnapshot()` 读取最新的 `effectiveContribution`
3. 为每个用户生成新的 `contributionRatio = effectiveContribution / networkTotal`
4. `mining-service` 消费快照，按新占比分配挖矿收益

**无需任何代码处理**——快照天然支持算力变更。

### 5.7 发布确认事件

处理完成后发布事件到 `contribution.transfer.adjusted`，供 transfer-service 确认 Saga 步骤。

### 5.8 新增文件清单

| 文件 | 说明 |
|------|------|
| `src/application/event-handlers/ownership-removed.handler.ts` | 消费 planting.ownership.removed |
| `src/application/event-handlers/ownership-added.handler.ts` | 消费 planting.ownership.added |
| `src/application/services/transfer-adjustment.service.ts` | 转让算力调整编排服务 |
| `src/domain/services/transfer-contribution-calculator.service.ts` | 转让算力计算（查询原始记录 + 生成对冲/新增流水） |

---

## 六、wallet-service 复用方案

wallet-service **无需新增代码**，完全复用现有能力：

### 6.1 冻结买方资金

transfer-service 调用 wallet-service 的 `freezeForPlanting` 或通用 `freeze` 接口：

```
POST /wallet/freeze
{
  accountSequence: buyer.accountSequence,
  amount: transferPrice,
  assetType: "USDT",
  refOrderId: transferOrderNo,
  memo: "树转让冻结"
}
```

对应 LedgerEntry：`entryType = FREEZE`

### 6.2 结算（转账给卖方 + 平台手续费）

transfer-service 调用 wallet-service 的内部转账：

```
// 1. 从买方冻结余额中扣款
POST /wallet/confirm-deduction
{
  accountSequence: buyer.accountSequence,
  amount: transferPrice,
  refOrderId: transferOrderNo
}

// 2. 转账给卖方（实收金额 = 转让价 - 手续费）
POST /wallet/internal-transfer
{
  fromAccountSequence: buyer.accountSequence,
  toAccountSequence: seller.accountSequence,
  amount: sellerReceiveAmount,
  refOrderId: transferOrderNo
}

// 3. 手续费归集（平台收入）
POST /wallet/fee-collection
{
  fromAccountSequence: buyer.accountSequence,
  toAccountSequence: "S0000000006",  // 手续费归集账户
  amount: platformFeeAmount,
  refOrderId: transferOrderNo
}
```

### 6.3 回滚（解冻）

```
POST /wallet/unfreeze
{
  accountSequence: buyer.accountSequence,
  amount: transferPrice,
  refOrderId: transferOrderNo,
  memo: "树转让取消，解冻"
}
```

---

## 七、完整事件契约

### 7.1 transfer-service 发出的事件

| 事件 | Topic | Key | 消费者 |
|------|-------|-----|--------|
| `TransferTreesLockRequest` | `transfer.trees.lock` | transferOrderNo | planting-service |
| `TransferOwnershipExecute` | `transfer.ownership.execute` | transferOrderNo | planting-service |
| `TransferTreesUnlockRequest` | `transfer.trees.unlock` | transferOrderNo | planting-service |
| `TransferPaymentFreezeRequest` | `transfer.payment.freeze` | transferOrderNo | wallet-service |
| `TransferPaymentSettleRequest` | `transfer.payment.settle` | transferOrderNo | wallet-service |
| `TransferPaymentUnfreezeRequest` | `transfer.payment.unfreeze` | transferOrderNo | wallet-service |

### 7.2 planting-service 发出的事件

| 事件 | Topic | Key | 消费者 |
|------|-------|-----|--------|
| `PlantingTransferLocked` | `planting.transfer.locked` | transferOrderNo | transfer-service |
| `PlantingTransferCompleted` | `planting.transfer.completed` | transferOrderNo | transfer-service |
| `PlantingOwnershipRemoved` | `planting.ownership.removed` | sellerAccountSequence | contribution-service, referral-service |
| `PlantingOwnershipAdded` | `planting.ownership.added` | buyerAccountSequence | contribution-service, referral-service |
| `PlantingTransferRolledBack` | `planting.transfer.rolledback` | transferOrderNo | transfer-service |

### 7.3 contribution-service 发出的事件

| 事件 | Topic | Key | 消费者 |
|------|-------|-----|--------|
| `TransferContributionAdjusted` | `contribution.transfer.adjusted` | transferOrderNo | transfer-service |

### 7.4 referral-service 发出的事件

| 事件 | Topic | Key | 消费者 |
|------|-------|-----|--------|
| `TransferStatsUpdated` | `referral.transfer.stats-updated` | transferOrderNo | transfer-service |

### 7.5 标准 Payload 格式

#### PlantingOwnershipRemoved Payload

```json
{
  "eventType": "PlantingOwnershipRemoved",
  "transferOrderNo": "TRF260218A3B7C9D1",
  "sourceOrderNo": "PLT2501150001A2B3",
  "sourceAdoptionId": "12345",
  "sellerUserId": "100001",
  "sellerAccountSequence": "D25011500001",
  "treeCount": 2,
  "contributionPerTree": "22684.1000000000",
  "selectedProvince": "440000",
  "selectedCity": "440100",
  "originalAdoptionDate": "2025-01-15",
  "originalExpireDate": "2027-01-16",
  "transferDate": "2026-02-18"
}
```

#### PlantingOwnershipAdded Payload

```json
{
  "eventType": "PlantingOwnershipAdded",
  "transferOrderNo": "TRF260218A3B7C9D1",
  "sourceOrderNo": "PLT2501150001A2B3",
  "sourceAdoptionId": "12345",
  "buyerUserId": "200002",
  "buyerAccountSequence": "D25121400002",
  "treeCount": 2,
  "contributionPerTree": "22684.1000000000",
  "selectedProvince": "440000",
  "selectedCity": "440100",
  "originalAdoptionDate": "2025-01-15",
  "originalExpireDate": "2027-01-16",
  "transferDate": "2026-02-18"
}
```

---

## 八、完整 Saga 流程图

```
┌─────────────────────────────────────────────────────────────────────┐
│                        transfer-service (Saga 编排器)                │
└────────────────────────────────┬────────────────────────────────────┘
                                 │
     Step 1: 买方发起转让         │
     ─────────────────────       │
     创建 TransferOrder          │
     status = PENDING            │
                                 │
     Step 2: 卖方确认             │
     ─────────────────────       │
     status = SELLER_CONFIRMED   │
                                 │
     Step 3: 冻结买方资金         │  ──── wallet-service ────
     ─────────────────────       │  freeze(buyer, transferPrice)
     status = PAYMENT_FROZEN     │  ← 确认冻结成功
                                 │
     Step 4: 锁定卖方树           │  ──── planting-service ────
     ─────────────────────       │  → transfer.trees.lock
     status = TREES_LOCKED       │  ← planting.transfer.locked
                                 │
     Step 5: 执行所有权变更       │  ──── planting-service ────
     ─────────────────────       │  → transfer.ownership.execute
     status = OWNERSHIP_         │  ← planting.transfer.completed
              TRANSFERRED        │
                                 │  planting-service 同时发布:
                                 │  → planting.ownership.removed
                                 │  → planting.ownership.added
                                 │
     Step 6: 等待算力调整确认     │  ──── contribution-service ────
     ─────────────────────       │  消费 planting.ownership.removed
     status = CONTRIBUTION_      │  消费 planting.ownership.added
              ADJUSTED           │  ← contribution.transfer.adjusted
                                 │
     Step 7: 等待统计更新确认     │  ──── referral-service ────
     ─────────────────────       │  消费 planting.ownership.removed
     status = STATS_UPDATED      │  消费 planting.ownership.added
                                 │  ← referral.transfer.stats-updated
                                 │
     Step 8: 结算资金             │  ──── wallet-service ────
     ─────────────────────       │  confirmDeduction(buyer)
     status = PAYMENT_SETTLED    │  internalTransfer(buyer→seller)
                                 │  feeCollection(buyer→S0000000006)
                                 │
     Step 9: 完成                 │
     ─────────────────────       │
     status = COMPLETED          │
                                 │
═══════════════════════════════════════════════════════════════════════
  补偿路径（从失败步骤反向执行）：
═══════════════════════════════════════════════════════════════════════
                                 │
  如果 Step 5/6/7 失败：         │
  → planting: transfer.trees.unlock (解锁树)
  → wallet: unfreeze (解冻资金)   │
                                 │
  如果 Step 4 失败：              │
  → wallet: unfreeze (解冻资金)   │
                                 │
  如果 Step 3 失败：              │
  → 直接标记 FAILED              │
```

---

## 九、补偿与回滚

### 9.1 补偿策略

| 失败步骤 | 需要补偿的操作 | 补偿方式 |
|---------|--------------|---------|
| 冻结资金失败 | 无 | 直接标记 FAILED |
| 锁定树失败 | 解冻买方资金 | wallet.unfreeze |
| 所有权变更失败 | 解锁树 + 解冻资金 | planting.unlock + wallet.unfreeze |
| 算力调整超时 | 重试 3 次后人工介入 | 算力为最终一致性，不阻塞流程 |
| 统计更新超时 | 重试 3 次后人工介入 | 统计为最终一致性，不阻塞流程 |
| 资金结算失败 | 反向变更所有权 + 解锁树 + 解冻资金 | 完整回滚 |

### 9.2 最终一致性策略

对于 Step 6（算力）和 Step 7（统计），采用**最终一致性**：

1. transfer-service 设置超时（5 分钟）
2. 超时后自动重试发送事件（最多 3 次）
3. 3 次重试后标记为 `CONTRIBUTION_PENDING` 或 `STATS_PENDING`
4. 定时任务扫描超时未完成的转让，发送告警
5. 管理员可通过 Admin API 手动触发重试或强制完成

### 9.3 幂等性保证

所有事件处理器通过 `ProcessedEvent` 表保证幂等性：

```typescript
const eventId = `transfer-${transferOrderNo}-${step}`;
const exists = await processedEventRepo.findByEventId(eventId);
if (exists) return; // 已处理，跳过
```

---

## 十、Admin Web 管理

### 10.1 转让管理页面

```
/admin/transfers          - 转让列表
/admin/transfers/:id      - 转让详情
```

#### 列表页功能

- 筛选条件：状态、日期范围、卖方/买方账号、转让编号
- 列表字段：转让编号、卖方、买方、棵数、金额、状态、创建时间
- 操作按钮：查看详情、强制取消（PENDING/SELLER_CONFIRMED 阶段）

#### 详情页功能

- 基本信息：转让双方、树信息、价格
- Saga 进度：每个步骤的状态和时间戳
- 算力流水：关联的 ContributionRecord 列表
- 团队统计变更：受影响的用户列表
- 操作日志：所有状态变更记录

### 10.2 Sidebar 入口

在 admin-web 的 Sidebar 中新增入口：

```
认种管理
  ├── 认种订单
  ├── 预种计划
  └── 转让管理    ← 新增
```

---

## 十一、Mobile App 用户界面

### 11.1 卖方入口

在持仓页面（PlantingPosition）的每棵树/每个订单旁新增「转让」按钮：

```
我的持仓（10棵树）
├── 订单 PLT250115... (5棵, 广东广州)
│   ├── 状态：挖矿中
│   └── [转让]
├── 订单 PLT250120... (3棵, 广东深圳)
│   ├── 状态：挖矿中
│   └── [转让]
└── 订单 PLT250201... (2棵, 北京朝阳)
    ├── 状态：挖矿中
    └── [转让]
```

### 11.2 转让发起页

```
┌──────────────────────────────┐
│  发起转让                     │
│                              │
│  转让订单：PLT250115...       │
│  棵数：5 棵（可调整）          │
│  树位置：广东广州              │
│  原始认种日期：2025-01-15     │
│  算力到期日：2027-01-16       │
│                              │
│  转让价格：______ USDT       │
│  平台手续费：2%               │
│  您将收到：______ USDT       │
│                              │
│  买方账号：D____________      │
│                              │
│  [确认转让]                   │
└──────────────────────────────┘
```

### 11.3 转让记录页

```
我的转让
├── [标签] 转出 | 转入 | 全部
├── TRF260218... 转出 5棵 → D25121400002
│   状态：已完成 | 收到 15000 USDT
│   2026-02-18 14:30
├── TRF260210... 转入 3棵 ← D25011500001
│   状态：已完成 | 支付 9500 USDT
│   2026-02-10 09:15
└── ...
```

---

## 十二、实施阶段计划

### Phase 1：基础框架（transfer-service + planting-service）

**目标**：完成转让订单的创建、锁定、所有权变更、资金结算的核心流程。

**交付内容**：
1. transfer-service 新微服务搭建
   - 项目脚手架（NestJS + Prisma + Kafka）
   - TransferOrder 数据模型 + Migration
   - Saga 状态机核心逻辑
   - REST API（用户端 + 管理端）
   - Outbox 事件发布

2. planting-service 纯新增
   - PlantingTransferRecord 数据表 + Migration
   - PlantingOrder.transferLockedCount 新增字段
   - 3 个事件处理器（lock / execute / rollback）
   - Outbox 事件发布（ownership.removed / ownership.added）

3. wallet-service 对接
   - 复用 freeze / unfreeze / internal-transfer 接口
   - 新增 LedgerEntryType: `TRANSFER_FREEZE` / `TRANSFER_PAYMENT`

4. Docker 配置
   - docker-compose.yml 新增 transfer-service
   - Kafka topic 创建脚本

**验收标准**：
- 完成转让全流程（不含算力和统计）
- 资金正确冻结、结算、回滚
- 持仓数量正确变更

---

### Phase 2：算力调整（contribution-service）

**目标**：完成算力的事务流水型增量调整。

**交付内容**：
1. contribution-service 纯新增
   - 新增 sourceType 枚举值（10 个 TRANSFER_OUT/IN 类型，含系统省市）
   - ContributionRecord.transferOrderNo 新增字段 + Migration
   - ownership-removed.handler.ts（卖方算力扣减）
   - ownership-added.handler.ts（买方算力分配）
   - transfer-adjustment.service.ts（编排服务）
   - transfer-contribution-calculator.service.ts（计算服务）

2. 解锁状态联动
   - 卖方 hasAdopted / unlockedLevelDepth / unlockedBonusTiers 重算
   - 买方 hasAdopted / unlockedLevelDepth / unlockedBonusTiers 更新
   - 买方推荐人 directReferralAdoptedCount 更新 + bonus 补发

3. 确认事件发布
   - `contribution.transfer.adjusted` 事件

**验收标准**：
- 卖方 effectiveContribution 正确减少
- 买方 effectiveContribution 正确增加
- 省市系统账户正确调整（卖方省市减少，买方省市增加）
- 全网算力总量守恒（卖方减少 = 买方增加 + UnallocatedContribution）
- 历史 ContributionRecord 零修改
- 次日快照正确反映新算力

---

### Phase 3：团队统计（referral-service）

**目标**：完成团队认种统计的增减和龙虎榜重算。

**交付内容**：
1. referral-service 纯新增
   - planting-transferred.handler.ts
   - TeamStatistics.removePersonalPlanting() 新增方法
   - TeamStatisticsRepository.batchUpdateTeamCountsForTransfer() 新增方法
   - 龙虎榜 max 重算逻辑修正（负数 delta 时全量扫描）

2. 确认事件发布
   - `referral.transfer.stats-updated` 事件

**验收标准**：
- 卖方链路 totalTeamPlantingCount 正确减少
- 买方链路 totalTeamPlantingCount 正确增加
- 龙虎榜排名正确更新
- selfPlantingCount 正确反映当前持仓

---

### Phase 4：前端（Admin Web + Mobile App）

**目标**：完成管理端和用户端的完整交互界面。

**交付内容**：
1. Admin Web（Next.js 14）
   - 转让管理列表页
   - 转让详情页（含 Saga 进度可视化）
   - Sidebar 入口
   - 统计看板

2. Mobile App（Flutter）
   - 持仓页转让入口
   - 转让发起页
   - 转让记录页（转出/转入）
   - 转让状态实时更新

**验收标准**：
- 完整的转让用户流程
- 管理端可查看所有转让记录和状态
- 异常状态有明确提示

---

### Phase 5：测试与上线

**交付内容**：
1. 单元测试
   - 算力计算正确性（卖方扣减 / 买方分配 / 解锁联动）
   - Saga 状态机全路径覆盖
   - 补偿逻辑验证

2. 集成测试
   - 完整转让流程 E2E
   - 补偿回滚 E2E
   - 并发转让冲突测试
   - 部分转让 + 多次转让场景

3. 压力测试
   - 大量并发转让请求
   - Kafka 事件积压恢复

4. 上线
   - 数据库 Migration 执行
   - 服务部署
   - 灰度放量

---

## 十三、算力流水示例

### 场景：A 转让 2 棵树给 B

**前提条件**：
- A 有 5 棵树（订单 PLT001），原始 contributionPerTree = 22617
- A 的上级链：R1 → R2 → R3（3 级，都已解锁）
- B 之前无树，B 的上级链：S1 → S2（2 级，S1 已解锁 5 级，S2 未解锁）
- 转让 2 棵

#### 卖方侧新增流水（全部为负数）

| # | accountSequence | sourceType | amount | remark |
|---|----------------|------------|--------|--------|
| 1 | A | TRANSFER_OUT_PERSONAL | -31,663.80 | 个人 22617×2×0.70 |
| 2 | R1 | TRANSFER_OUT_TEAM_LEVEL (L1) | -226.17 | R1 第1级 22617×2×0.005 |
| 3 | R2 | TRANSFER_OUT_TEAM_LEVEL (L2) | -226.17 | R2 第2级 |
| 4 | R3 | TRANSFER_OUT_TEAM_LEVEL (L3) | -226.17 | R3 第3级 |
| 5 | A | TRANSFER_OUT_BONUS (T1) | -1,130.85 | 加成T1 22617×2×0.025 |
| 6 | A | TRANSFER_OUT_BONUS (T2) | -1,130.85 | 加成T2（如已解锁） |
| 7 | SystemAccount(PROVINCE,440000) | TRANSFER_OUT_SYSTEM_PROVINCE | -452.34 | 省公司 22617×2×0.01 |
| 8 | SystemAccount(CITY,440100) | TRANSFER_OUT_SYSTEM_CITY | -904.68 | 市公司 22617×2×0.02 |

#### 买方侧新增流水（全部为正数）

假设买方 B 在北京朝阳（110000/110105），与卖方不同省市：

| # | accountSequence | sourceType | amount | remark |
|---|----------------|------------|--------|--------|
| 9 | B | TRANSFER_IN_PERSONAL | +31,663.80 | 个人 |
| 10 | S1 | TRANSFER_IN_TEAM_LEVEL (L1) | +226.17 | S1 第1级（已解锁） |
| 11 | S1 | TRANSFER_IN_TEAM_LEVEL (L2) | +226.17 | S1 第2级（已解锁） |
| 12 | S1 | TRANSFER_IN_TEAM_LEVEL (L3) | +226.17 | S1 第3级（已解锁） |
| 13 | S1 | TRANSFER_IN_TEAM_LEVEL (L4) | +226.17 | S1 第4级（已解锁） |
| 14 | S1 | TRANSFER_IN_TEAM_LEVEL (L5) | +226.17 | S1 第5级（已解锁） |
| 15 | → UnallocatedContribution | LEVEL_OVERFLOW (L6) | 226.17 | S2 第6级（未解锁） |
| 16 | → UnallocatedContribution | LEVEL_OVERFLOW (L7) | 226.17 | S2 第7级（未解锁） |
| ... | ... | ... | ... | L8-L15 类似 |
| 17 | B | TRANSFER_IN_BONUS (T1) | +1,130.85 | 加成T1（买方获树后解锁） |
| 18 | → UnallocatedContribution | BONUS_TIER_2_PENDING | 1,130.85 | T2（买方未达条件） |
| 19 | → UnallocatedContribution | BONUS_TIER_3_PENDING | 1,130.85 | T3（买方未达条件） |
| 20 | SystemAccount(PROVINCE,110000) | TRANSFER_IN_SYSTEM_PROVINCE | +452.34 | 买方省公司（北京） |
| 21 | SystemAccount(CITY,110105) | TRANSFER_IN_SYSTEM_CITY | +904.68 | 买方市公司（朝阳） |

#### 解锁状态联动

| 用户 | 变更前 | 变更后 | 说明 |
|------|--------|--------|------|
| A | hasAdopted=true | hasAdopted=true | A 还剩 3 棵 |
| B | hasAdopted=false | hasAdopted=true | 获得树后解锁 |
| B 的推荐人 | directAdoptedCount=N | directAdoptedCount=N+1 | 可能触发升档补发 |

---

## 十四、风险评估与应对

| 风险 | 影响 | 应对措施 |
|------|------|---------|
| Saga 步骤失败后补偿不完整 | 资金或树处于不一致状态 | 定时任务扫描异常状态 + Admin 手动介入 |
| 并发转让同一订单 | 超卖 | PlantingOrder.transferLockedCount 乐观锁 + 数据库约束 |
| contribution 事件丢失 | 算力不一致 | Outbox + ProcessedEvent 双保障 + 定时对账 |
| referral 统计出现负数 | 龙虎榜异常 | Math.max(0, ...) 兜底 + 告警 |
| 全网算力总量不守恒 | 挖矿分配异常 | 每日对账脚本：∑增加 = ∑减少 + ∑UnallocatedContribution |
| 大量转让导致 Kafka 积压 | 处理延迟 | 独立 Consumer Group + 限流 |
| 卖方转让后 selfPlantingCount=0 影响推荐人解锁 | 推荐人未来新认种的分配比例降低 | 这是正确的业务行为，无需应对 |

---

## 十五、对账与监控

### 15.1 每日对账任务

```sql
-- 算力守恒检查
-- 对于每一笔转让，验证：|卖方扣减| = |买方增加| + |UnallocatedContribution|
SELECT
  t.transfer_order_no,
  SUM(CASE WHEN cr.source_type LIKE 'TRANSFER_OUT_%' THEN ABS(cr.amount) ELSE 0 END) AS total_out,
  SUM(CASE WHEN cr.source_type LIKE 'TRANSFER_IN_%' THEN cr.amount ELSE 0 END) AS total_in,
  SUM(CASE WHEN uc.transfer_order_no IS NOT NULL THEN uc.amount ELSE 0 END) AS total_unallocated
FROM transfer_orders t
LEFT JOIN contribution_records cr ON cr.transfer_order_no = t.transfer_order_no
LEFT JOIN unallocated_contributions uc ON uc.transfer_order_no = t.transfer_order_no
WHERE t.status = 'COMPLETED'
GROUP BY t.transfer_order_no
HAVING ABS(total_out - total_in - total_unallocated) > 0.01;
-- 结果应为空（无差异）
```

### 15.2 监控告警

- Saga 步骤超时（> 5 分钟未完成）
- 补偿失败（ROLLING_BACK 状态超过 10 分钟）
- 算力守恒偏差 > 0.01
- 团队统计出现负数
- 每日转让量异常波动

---

## 十六、积分股转让（2.0 系统附加功能）

### 16.1 业务规则

| 规则 | 说明 |
|------|------|
| **正常情况** | 2.0 系统**不支持**用户间积分股（Share）转让 |
| **触发条件** | 树转让完成（TransferOrder.status = COMPLETED）后自动开启 |
| **参与方限制** | 仅限该笔树转让的买方和卖方双向互转 |
| **有效期** | 树转让完成后 24 小时内 |
| **到期处理** | 24 小时后窗口自动关闭，不可再发起 Share 转让 |
| **转让方向** | 双向：卖方→买方、买方→卖方 均可 |
| **转让次数** | 窗口期内不限次数 |
| **转让数量** | 不超过发起方的可用 Share 余额 |

### 16.2 数据模型（mining-wallet-service 新增）

#### ShareTransferWindow（积分股转让窗口）

```prisma
model ShareTransferWindow {
  id                     String    @id @default(uuid())
  transferOrderNo        String    @unique @map("transfer_order_no") @db.VarChar(50)  // 关联的树转让订单号

  partyAAccountSequence  String    @map("party_a_account_sequence") @db.VarChar(20)   // 卖方
  partyBAccountSequence  String    @map("party_b_account_sequence") @db.VarChar(20)   // 买方

  status                 String    @default("ACTIVE") @map("status") @db.VarChar(20)  // ACTIVE / EXPIRED / CLOSED
  openedAt               DateTime  @default(now()) @map("opened_at")
  expiresAt              DateTime  @map("expires_at")                                 // openedAt + 24h
  closedAt               DateTime? @map("closed_at")

  createdAt              DateTime  @default(now()) @map("created_at")

  @@index([partyAAccountSequence])
  @@index([partyBAccountSequence])
  @@index([status, expiresAt])
  @@map("share_transfer_windows")
}
```

#### ShareTransferRecord（积分股转让记录）

```prisma
model ShareTransferRecord {
  id                     String    @id @default(uuid())
  windowId               String    @map("window_id")                                  // 关联窗口
  transferOrderNo        String    @map("transfer_order_no") @db.VarChar(50)          // 关联的树转让订单号

  fromAccountSequence    String    @map("from_account_sequence") @db.VarChar(20)
  toAccountSequence      String    @map("to_account_sequence") @db.VarChar(20)
  amount                 Decimal   @map("amount") @db.Decimal(30, 8)                  // Share 数量

  status                 String    @default("COMPLETED") @map("status") @db.VarChar(20)
  createdAt              DateTime  @default(now()) @map("created_at")

  @@index([windowId])
  @@index([transferOrderNo])
  @@index([fromAccountSequence])
  @@index([toAccountSequence])
  @@map("share_transfer_records")
}
```

### 16.3 流程

```
树转让完成 (TransferOrder.status = COMPLETED)
    ↓
transfer-service 发布 Outbox 事件：
    → transfer.share-window.open
    payload: { transferOrderNo, sellerAccountSeq, buyerAccountSeq }
    ↓
mining-wallet-service 消费事件：
    → 创建 ShareTransferWindow (status=ACTIVE, expiresAt=now+24h)
    ↓
24小时内，双方通过 API 发起 Share 转让：
    POST /share-transfers
    {
      transferOrderNo: "TRF260218A3B7C9D1",
      toAccountSequence: "D25121400002",
      amount: 1000.00
    }
    ↓
mining-wallet-service 处理：
    1. 查找 ACTIVE 状态的 ShareTransferWindow
    2. 验证发起方和接收方都在窗口的 partyA/partyB 中
    3. 验证 expiresAt > now()
    4. 调用已有的 transferBetweenUsers() 执行 Share 转账
    5. 创建 ShareTransferRecord 记录
    6. 记录 UserWalletTransaction (transactionType=TRANSFER_OUT/TRANSFER_IN)
    ↓
24小时到期：
    → 定时任务扫描 expiresAt < now() 且 status=ACTIVE 的窗口
    → 更新 status = EXPIRED
```

### 16.4 API 设计（mining-wallet-service 新增）

```
POST   /share-transfers                    - 发起积分股转让
GET    /share-transfers/window/:orderNo    - 查询窗口状态和剩余时间
GET    /share-transfers/records            - 查询我的积分股转让记录
```

#### POST /share-transfers 请求体

```json
{
  "transferOrderNo": "TRF260218A3B7C9D1",
  "toAccountSequence": "D25121400002",
  "amount": 1000.00000000
}
```

#### POST /share-transfers 响应

```json
{
  "success": true,
  "data": {
    "recordId": "uuid",
    "fromAccountSequence": "D25011500001",
    "toAccountSequence": "D25121400002",
    "amount": "1000.00000000",
    "windowExpiresAt": "2026-02-19T14:30:00Z",
    "createdAt": "2026-02-18T15:00:00Z"
  }
}
```

#### GET /share-transfers/window/:orderNo 响应

```json
{
  "success": true,
  "data": {
    "transferOrderNo": "TRF260218A3B7C9D1",
    "status": "ACTIVE",
    "partyA": "D25011500001",
    "partyB": "D25121400002",
    "openedAt": "2026-02-18T14:30:00Z",
    "expiresAt": "2026-02-19T14:30:00Z",
    "remainingSeconds": 72000,
    "records": [
      { "from": "D25011500001", "to": "D25121400002", "amount": "1000.00", "at": "..." }
    ]
  }
}
```

### 16.5 mining-wallet-service 新增文件

| 文件 | 说明 |
|------|------|
| `src/application/services/share-transfer.service.ts` | 积分股转让业务服务 |
| `src/application/event-handlers/share-window-opened.handler.ts` | 消费树转让完成事件，创建窗口 |
| `src/application/schedulers/share-window-expiry.scheduler.ts` | 定时任务：过期窗口关闭 |
| `src/api/controllers/share-transfer.controller.ts` | REST API |

### 16.6 Mobile App 界面

树转让完成后，在转让详情页底部显示积分股转让入口：

```
┌──────────────────────────────┐
│  转让已完成 ✓                 │
│                              │
│  ┌────────────────────────┐  │
│  │ 积分股转让窗口           │  │
│  │ 剩余时间：18:45:30      │  │
│  │                        │  │
│  │ 您的 Share：12,345.67   │  │
│  │ 对方 Share：8,901.23    │  │
│  │                        │  │
│  │ 转让数量：________      │  │
│  │                        │  │
│  │ [转给对方]              │  │
│  └────────────────────────┘  │
│                              │
│  转让记录：                   │
│  • 您 → 对方 1,000 Share     │
│  • 对方 → 您 500 Share       │
└──────────────────────────────┘
```

### 16.7 实施归属

积分股转让功能归入 **Phase 2（算力调整）** 阶段，因为：
- 依赖树转让完成事件
- 在 mining-wallet-service（2.0）中实现
- 复用已有的 `transferBetweenUsers()` 方法
- 只需新增窗口管理和验证逻辑

---

## 附录 A：配置项

| 配置项 | 默认值 | 说明 |
|--------|--------|------|
| TRANSFER_PLATFORM_FEE_RATE | 0.02 | 平台手续费率 2% |
| TRANSFER_MIN_PRICE_PER_TREE | 0 | 每棵树最低转让价 |
| TRANSFER_MAX_PRICE_PER_TREE | 99999 | 每棵树最高转让价 |
| TRANSFER_SAGA_STEP_TIMEOUT_MS | 300000 | Saga 单步超时 5 分钟 |
| TRANSFER_SAGA_MAX_RETRIES | 3 | Saga 步骤最大重试次数 |
| TRANSFER_ENABLED | true | 转让功能总开关 |
| TRANSFER_DAILY_LIMIT_PER_USER | 10 | 每用户每日转让次数上限 |
| TRANSFER_COOLDOWN_DAYS | 0 | 认种后 N 天内不可转让 |
| SHARE_TRANSFER_WINDOW_HOURS | 24 | 积分股转让窗口有效期（小时） |
| SHARE_TRANSFER_ENABLED | true | 积分股转让功能开关 |

## 附录 B：新增 Kafka Topic 列表

| Topic | 生产者 | 消费者 |
|-------|--------|--------|
| `transfer.trees.lock` | transfer-service | planting-service |
| `transfer.trees.lock.ack` | planting-service | transfer-service |
| `transfer.ownership.execute` | transfer-service | planting-service |
| `transfer.trees.unlock` | transfer-service | planting-service |
| `transfer.trees.unlock.ack` | planting-service | transfer-service |
| `planting.transfer.completed` | planting-service | transfer-service |
| `planting.transfer.rolledback` | planting-service | transfer-service |
| `planting.ownership.removed` | planting-service | contribution-service, referral-service |
| `planting.ownership.added` | planting-service | contribution-service, referral-service |
| `contribution.transfer.adjusted` | contribution-service | transfer-service |
| `referral.transfer.stats-updated` | referral-service | transfer-service |
| `transfer.share-window.open` | transfer-service | mining-wallet-service |

## 附录 C：新增数据库表汇总

| 服务 | 表名 | 说明 |
|------|------|------|
| transfer-service | transfer_orders | 转让订单（Saga 聚合根） |
| transfer-service | transfer_status_logs | 状态变更日志 |
| transfer-service | outbox_events | Outbox 事件 |
| transfer-service | processed_events | 幂等性检查 |
| planting-service | planting_transfer_records | 转让记录 |
| mining-wallet-service | share_transfer_windows | 积分股转让窗口（24小时有效期） |
| mining-wallet-service | share_transfer_records | 积分股转让记录 |

## 附录 D：现有表新增字段汇总

| 服务 | 表名 | 新增字段 | 类型 | 说明 |
|------|------|---------|------|------|
| planting-service | planting_orders | transfer_locked_count | Int DEFAULT 0 | 被转让锁定的棵数 |
| contribution-service | contribution_records | transfer_order_no | VARCHAR(50) NULL | 关联转让订单号 |
| contribution-service | unallocated_contributions | transfer_order_no | VARCHAR(50) NULL | 关联转让订单号 |
