# Registration Invite Code Architecture

本文定义 App 注册邀请码能力。用户注册时可以选填邀请码；如果该邀请码属于某个有效用户，则该用户成为新用户的邀请人。邀请码归因是用户关系事实，不能只把字符串放在 `users.invite_code` 里。

## Goals

- 每个用户有一个可展示、可复制的永久邀请码。
- 新用户注册或完成资料时可以选填邀请码。
- 非空邀请码必须能解析到同一 `app_code`、同一 `region_id` 下的有效邀请人。
- 邀请关系一旦绑定不可被客户端修改。
- 邀请关系必须落 MySQL；Redis 只能做短 TTL 查询缓存。
- 邀请统计必须拆成 `invite_count` 和 `valid_invite_count`：前者表示成功填码绑定关系的人数，后者表示被邀请人累计充值达到后台配置门槛的人数。
- 后续奖励、活动、风控和运营统计都从邀请关系事实消费，不反推历史字符串。

## Non-Goals

- 首版不在注册链路内同步发奖励。
- 首版不支持用户自己修改邀请码。
- 首版不支持跨 App 邀请。
- 首版不把邀请码等同于 Agency、BD、币商或主播组织关系。
- 首版不做邀请排行榜；只保存事实和提供轻量查询。
- 首版不在注册链路内同步判断有效邀请；有效邀请由钱包充值事实异步推进。

## Ownership

| Capability | Owner | Rule |
| --- | --- | --- |
| 邀请码生成 | `user-service` | 新用户创建时生成自己的邀请码 |
| 邀请码解析 | `user-service` | 注册事务内按 `app_code + code` 查询有效 code |
| 邀请关系绑定 | `user-service` | 一个被邀请用户最多一条关系 |
| 外部 HTTP | `gateway-service` | 转发 `invite_code`，不自行解析归属 |
| 邀请人数统计 | `user-service` invite module | `invite_count` 来自 `user_invite_relations` 绑定事实 |
| 有效邀请判定 | `user-service` invite module + `wallet-service` recharge event | 钱包只产出充值事实，邀请模块按后台策略累计并标记有效 |
| 我的页展示 | `gateway-service` + `user-service` | `overview` 返回当前用户自己的邀请码、邀请人数和有效邀请人数 |
| 奖励/通知 | 后续活动或钱包模块 | 通过 outbox/event 异步消费，不阻塞注册 |

## Product Rules

### Code Ownership

- 邀请码属于用户，不属于设备、session、三方账号或 Agency。
- 邀请码按 `app_code + region_id` 隔离；`lalu` 的邀请码不能邀请 `popoo` 用户，不同区域用户也不能互相绑定邀请关系。
- 用户被禁用或封禁后，默认不允许继续作为新邀请人的有效 code owner；历史邀请关系不删除。
- 用户自己的邀请码创建后长期稳定。后续如果允许换码，必须保留旧码和历史关系快照。

### Binding Rules

| Case | Result |
| --- | --- |
| 注册不填邀请码 | 创建用户，不创建邀请关系 |
| 注册填写有效邀请码 | 创建用户，并绑定 `inviter_user_id` |
| 注册填写不存在、停用、跨 App、跨区域或 owner 非 active 的邀请码 | 返回 `INVALID_INVITE_CODE`，不静默忽略 |
| 用户填写自己的邀请码 | 返回 `INVALID_INVITE_CODE` |
| 已有邀请关系的用户再次提交邀请码 | 不修改原关系 |
| 已完成注册用户提交邀请码 | 不修改原关系；如果通过 onboarding 入口提交，按已完成资料逻辑不处理邀请码 |
| 已存在三方身份再次登录并带新邀请码 | 不创建、不修改邀请关系 |

非空邀请码失败时选择“失败注册”而不是“忽略邀请码”，原因是用户和渠道都需要明确知道归因没有成功，避免后续奖励和申诉没有事实依据。

### Count Rules

邀请统计必须拆成两个指标：

| Metric | Meaning | Source | Update Timing |
| --- | --- | --- | --- |
| `invite_count` | 邀请人数，表示有多少用户成功填写邀请码并绑定到当前邀请人 | `user_invite_relations` | 绑定关系成功后增加 |
| `valid_invite_count` | 有效邀请人数，表示有多少被邀请用户累计成功充值金币达到策略门槛 | `user_invite_recharge_progress.status=valid` | 钱包充值事件累计达到门槛后增加 |

规则：

- `invite_count` 只看填码绑定事实，不要求被邀请人充值、消费、上麦或进入房间。
- `valid_invite_count` 默认门槛是被邀请人累计成功充值 `80000` `COIN`，门槛必须由后台策略配置，不能写死在代码里。
- 有效邀请只统计钱包标记为“充值”的 `COIN` 账务事实；送礼收入、奖励、后台普通调账、资源赠送和当前余额变化都不能直接计入。
- 币商给玩家转金币如果已经由 `wallet-service.wallet_recharge_records` 记录为用户充值，可以计入；是否计入 Apple/Google、线下人工充值、币商转账等类型由策略的 `eligible_recharge_types` 控制。
- 同一个被邀请用户只能让邀请人成为一次有效邀请；充值超过门槛后不重复增加。
- 有效状态一旦达成，首版不因后续消费减少而回退；如果后续接入退款、拒付或作弊处理，需要后台风控撤销或新增负向事件重算。
- 邀请人后续被封禁不删除历史关系和历史有效事实，但可以让其邀请码失效，避免继续产生新关系。

## App Flow

首版兼容两种客户端采集时机：

1. 客户端在三方登录前已经拿到邀请码：提交到 `POST /api/v1/auth/third-party/login`。
2. 客户端在资料补全页让用户输入邀请码：提交到 `POST /api/v1/users/me/onboarding/complete`。

推荐 App 把邀请码放在资料补全页，因为当前架构是“三方登录即创建未完成用户”，注册页用户资料都在 `CompleteOnboarding` 原子提交。

```mermaid
sequenceDiagram
  participant C as Client
  participant G as gateway-service
  participant U as user-service
  participant DB as hyapp_user

  C->>G: POST /auth/third-party/login(provider, credential, optional invite_code)
  G->>U: LoginThirdParty
  U->>U: verify provider credential
  alt new third-party identity
    U->>DB: create users + own user_invite_codes
    opt invite_code present
      U->>DB: resolve invite code and insert user_invite_relations
    end
    U->>DB: create third_party_identity + auth_session
  else existing identity
    U->>DB: create auth_session only
  end
  U-->>G: token(profile_completed=false/true)

  C->>G: POST /users/me/onboarding/complete(username, avatar, country, optional invite_code)
  G->>U: CompleteOnboarding
  U->>DB: lock users row
  opt no existing invite relation and invite_code present
    U->>DB: resolve invite code and insert user_invite_relations
  end
  U->>DB: update profile_completed=true
  U-->>G: new access_token(profile_completed=true)
```

## HTTP Contract

### Login Or Register

```http
POST /api/v1/auth/third-party/login
```

`invite_code` 仍然是可选字段。只有本次请求首次创建用户时，该字段才可能绑定邀请关系；已有用户登录时忽略该字段，不允许通过重新登录改归因。

### Complete Onboarding

```http
POST /api/v1/users/me/onboarding/complete
Authorization: Bearer <access_token>
```

Request:

```json
{
  "username": "Alice",
  "avatar": "https://cdn.example/avatar.png",
  "gender": "female",
  "country": "SG",
  "invite_code": "A1B2C3"
}
```

Response:

```json
{
  "user_id": "918274650129384448",
  "profile_completed": true,
  "invite": {
    "bound": true,
    "invite_code": "A1B2C3",
    "inviter_user_id": "10001"
  },
  "token": {
    "access_token": "jwt_xxx",
    "profile_completed": true
  }
}
```

`invite` 可以首版只返回 `bound`，但服务端必须在库里保存 `inviter_user_id`。客户端不能根据返回内容自行计算邀请关系。

### My Invite Code

首屏可以直接跟随 `GET /api/v1/users/me/overview` 返回：

```json
{
  "invite": {
    "my_invite_code": "A1B2C3",
    "invite_enabled": true,
    "invite_count": 12,
    "valid_invite_count": 3,
    "valid_invite_threshold_coin": 80000
  }
}
```

`invite_count` 和 `valid_invite_count` 必须来自预计算 read model。首版没有 read model 时返回 `0` 或不返回，不能为了首页展示实时 `COUNT(*)` 扫邀请关系或充值流水。`invited_count` 这个旧命名语义不清，后续对外接口应收敛为 `invite_count`。

## Data Model

### `user_invite_codes`

```sql
CREATE TABLE user_invite_codes (
  app_code VARCHAR(32) NOT NULL,
  invite_code_id BIGINT NOT NULL,
  code VARCHAR(64) NOT NULL,
  owner_user_id BIGINT NOT NULL,
  status VARCHAR(32) NOT NULL,
  is_primary TINYINT(1) NOT NULL DEFAULT 1,
  created_at_ms BIGINT NOT NULL,
  updated_at_ms BIGINT NOT NULL,
  PRIMARY KEY (app_code, invite_code_id),
  UNIQUE KEY uk_user_invite_codes_code (app_code, code),
  KEY idx_user_invite_codes_owner (app_code, owner_user_id, status)
);
```

字段规则：

- `code` 入库前 trim、转大写，只允许 `A-Z0-9`，长度建议 6 到 12。
- `status` 首版只需要 `active`、`disabled`。
- 一个 owner 首版只创建一个 primary code。MySQL 无法跨状态表达复杂唯一时，先通过事务锁用户行和查询保证；后续可加 generated column 做 active primary 唯一。
- code 不使用 `users.current_display_user_id`，避免靓号、短号过期或短号运营变更影响邀请关系。

### `user_invite_relations`

```sql
CREATE TABLE user_invite_relations (
  app_code VARCHAR(32) NOT NULL,
  invited_user_id BIGINT NOT NULL,
  inviter_user_id BIGINT NOT NULL,
  invite_code_id BIGINT NOT NULL,
  invite_code VARCHAR(64) NOT NULL,
  source VARCHAR(32) NOT NULL,
  bound_at_ms BIGINT NOT NULL,
  request_id VARCHAR(96) NOT NULL,
  created_at_ms BIGINT NOT NULL,
  PRIMARY KEY (app_code, invited_user_id),
  KEY idx_user_invite_relations_inviter (app_code, inviter_user_id, bound_at_ms),
  KEY idx_user_invite_relations_code (app_code, invite_code, bound_at_ms)
);
```

字段规则：

- `invited_user_id` 主键保证一个用户只能被归因一次。
- `invite_code` 是绑定时的快照；即使后续 code 被禁用或换码，历史关系仍可解释。
- `source` 首版可取 `login` 或 `onboarding`。
- 历史关系不因为邀请人被封禁、改资料或换码而删除。

### `user_invite_validity_policies`

有效邀请门槛必须由后台配置。默认策略是累计成功充值 `80000` `COIN` 后成为有效邀请。

```sql
CREATE TABLE user_invite_validity_policies (
  app_code VARCHAR(32) NOT NULL,
  policy_id BIGINT NOT NULL,
  policy_version VARCHAR(64) NOT NULL,
  status VARCHAR(32) NOT NULL,
  region_id BIGINT NOT NULL DEFAULT 0,
  required_recharge_coin_amount BIGINT NOT NULL,
  eligible_recharge_types JSON NOT NULL,
  effective_from_ms BIGINT NOT NULL,
  effective_to_ms BIGINT NULL,
  created_by_user_id BIGINT NOT NULL,
  created_at_ms BIGINT NOT NULL,
  updated_at_ms BIGINT NOT NULL,
  PRIMARY KEY (app_code, policy_id),
  UNIQUE KEY uk_user_invite_validity_policy_version (app_code, policy_version),
  KEY idx_user_invite_validity_policy_active (app_code, region_id, status, effective_from_ms)
);
```

字段规则：

- `required_recharge_coin_amount` 默认配置为 `80000`，单位是钱包 `COIN` 最小整数单位。
- `region_id=0` 表示全局默认策略；有区域策略时按被邀请用户注册/当前业务区域选择，具体选择规则必须写入策略说明。
- `eligible_recharge_types` 控制哪些充值类型可计入有效邀请，例如 `provider_iap`、`coin_seller_transfer`、`manual_recharge`。奖励、礼物收入、活动赠送和普通调账默认不计入。
- 策略修改只能影响新达成有效邀请的判断；已达成的 `valid` 事实不自动重算。

### `user_invite_recharge_progress`

该表保存每个被邀请用户距离有效邀请的累计进度。它不是钱包余额来源，只消费钱包充值事实。

```sql
CREATE TABLE user_invite_recharge_progress (
  app_code VARCHAR(32) NOT NULL,
  invited_user_id BIGINT NOT NULL,
  inviter_user_id BIGINT NOT NULL,
  policy_id BIGINT NOT NULL,
  policy_version VARCHAR(64) NOT NULL,
  required_recharge_coin_amount BIGINT NOT NULL,
  accumulated_recharge_coin_amount BIGINT NOT NULL,
  status VARCHAR(32) NOT NULL,
  valid_at_ms BIGINT NULL,
  valid_transaction_id VARCHAR(96) NOT NULL DEFAULT '',
  created_at_ms BIGINT NOT NULL,
  updated_at_ms BIGINT NOT NULL,
  PRIMARY KEY (app_code, invited_user_id),
  KEY idx_user_invite_progress_inviter_status (app_code, inviter_user_id, status, valid_at_ms),
  KEY idx_user_invite_progress_updated (app_code, status, updated_at_ms)
);
```

字段规则：

- `status` 首版使用 `pending` 和 `valid`。
- `accumulated_recharge_coin_amount` 只累计符合策略的成功充值金币数量。
- `valid_transaction_id` 保存让累计金额首次达到门槛的钱包交易 ID，便于审计。
- 同一个 `invited_user_id` 只能有一条进度；达到 `valid` 后不再重复增加 `valid_invite_count`。

### `user_invite_event_consumption`

钱包充值事件可能重投，邀请模块必须记录消费幂等。

```sql
CREATE TABLE user_invite_event_consumption (
  app_code VARCHAR(32) NOT NULL,
  source VARCHAR(64) NOT NULL,
  event_id VARCHAR(128) NOT NULL,
  transaction_id VARCHAR(96) NOT NULL,
  user_id BIGINT NOT NULL,
  status VARCHAR(32) NOT NULL,
  error_message VARCHAR(512) NOT NULL DEFAULT '',
  consumed_at_ms BIGINT NULL,
  created_at_ms BIGINT NOT NULL,
  updated_at_ms BIGINT NOT NULL,
  PRIMARY KEY (app_code, source, event_id),
  KEY idx_user_invite_event_tx (app_code, transaction_id),
  KEY idx_user_invite_event_status (app_code, status, updated_at_ms)
);
```

同一 `transaction_id` 如果通过不同事件重复到达，也不能重复累计；实现时应在同一事务中检查 `event_id` 和 `transaction_id` 的已消费状态。

### `user_invite_counters`

我的页和后台列表不能实时扫关系表或钱包流水，应读取计数 read model。

```sql
CREATE TABLE user_invite_counters (
  app_code VARCHAR(32) NOT NULL,
  inviter_user_id BIGINT NOT NULL,
  invite_count BIGINT NOT NULL DEFAULT 0,
  valid_invite_count BIGINT NOT NULL DEFAULT 0,
  updated_at_ms BIGINT NOT NULL,
  PRIMARY KEY (app_code, inviter_user_id)
);
```

字段规则：

- `invite_count` 由 `UserInvited` 事实推进。
- `valid_invite_count` 由 `UserInviteBecameValid` 事实推进。
- 计数写入必须幂等；重复消费同一事件不能重复增加。
- 如果计数异常，允许后台重算：`invite_count` 从 `user_invite_relations` 汇总，`valid_invite_count` 从 `user_invite_recharge_progress.status='valid'` 汇总。

### `users.invite_code`

现有 `users.invite_code` 只保留“注册时提交的邀请码字符串快照”。它不能作为权威邀请关系来源，因为它没有 `inviter_user_id`，也无法处理 code 换绑、停用、跨 App 和历史审计。

新代码应优先读取 `user_invite_relations`；`users.invite_code` 可以继续保留用于兼容和排障。

## Transaction Rules

新用户创建事务内建议顺序：

1. 校验 provider credential。
2. 锁定或分配 `user_id` 和默认短号。
3. 创建 `users`。
4. 创建用户自己的 `user_invite_codes`。
5. 如果请求带 `invite_code`，解析并锁定 code owner 的 `users` 行。
6. 校验 owner active、非自己、同 `app_code`、同 `region_id`。
7. 插入 `user_invite_relations`。
8. 创建 `third_party_identities` 和 `auth_sessions`。
9. 写 `UserRegistered` / `UserInvited` outbox 事件。

`CompleteOnboarding` 绑定邀请码时：

1. 锁当前 `users` 行。
2. 如果 `profile_completed=true`，不允许再绑定邀请码。
3. 如果已有 `user_invite_relations`，不修改原关系。
4. 如果请求带非空 `invite_code`，解析 code，锁 inviter 用户行，插入关系。
5. 更新资料、国家、区域和 `profile_completed`。
6. 返回新的 access token。

关系插入和资料完成必须在同一事务内提交，避免用户看到注册完成但邀请关系丢失。

邀请关系绑定成功后，同事务或同一 outbox 消费链路需要初始化 `user_invite_recharge_progress(status=pending)` 并幂等增加 `user_invite_counters.invite_count`。如果计数更新失败，关系事实不能回滚；必须通过 `UserInvited` outbox 重试补偿计数。

## Effective Invite Flow

有效邀请不在注册链路内计算。它由钱包充值事实异步推进：

```mermaid
sequenceDiagram
  participant W as wallet-service
  participant WO as wallet_outbox
  participant C as invite consumer
  participant U as user-service invite module
  participant DB as hyapp_user

  W->>W: commit successful COIN recharge
  W->>WO: WalletRechargeRecorded(user_id, coin_amount, recharge_type)
  C->>WO: consume recharge event
  C->>U: ApplyInviteRecharge(user_id, transaction_id, coin_amount)
  U->>DB: find user_invite_relations by invited_user_id
  alt relation exists and recharge_type eligible
    U->>DB: lock user_invite_recharge_progress
    U->>DB: accumulate recharge coin amount
    opt first time accumulated >= required threshold
      U->>DB: mark status=valid and valid_at_ms
      U->>DB: increment user_invite_counters.valid_invite_count
      U->>DB: write UserInviteBecameValid outbox
    end
  else no relation or ineligible recharge
    U-->>C: idempotent skipped
  end
```

处理规则：

- 钱包只负责产出充值事实，不理解邀请关系，也不直接增加邀请统计。
- 邀请模块按 `app_code + transaction_id` 对充值事件做幂等消费，同一充值事件不能重复累计。
- 被邀请用户未绑定邀请关系时，充值事件跳过，不创建关系。
- 充值事件早于邀请关系绑定时不应补算，除非后台明确执行重算任务；正常链路要求先填邀请码、后充值才算有效邀请。
- 达到门槛的判断使用累计充值金币数量，不使用当前 `COIN` 余额；用户充值后消费完仍然可以成为有效邀请。
- 如果后续接入退款/拒付事件，需要新增负向充值处理策略；首版只处理正向成功充值。

## Code Generation

邀请码生成建议：

- 使用服务端随机生成，不让客户端提交自己的 code。
- 字符集使用 `ABCDEFGHJKLMNPQRSTUVWXYZ23456789`，去掉容易混淆的 `0/O/1/I`。
- 首版 8 位即可；如果发生唯一冲突，最多重试 5 次，仍失败返回内部错误并告警。
- 不把手机号、邮箱、设备号、短号直接作为邀请码。

## Caching

注册量没有明显压力前，邀请码解析直接查 MySQL。

如果后续加入 Redis：

| Key | Value | TTL | Rule |
| --- | --- | --- | --- |
| `invite_code:{app_code}:{code}` | `owner_user_id,status,version` | 5-30 min | 只做加速 |

Redis miss 或异常时必须回 MySQL 或 fail-closed，不能把不存在当有效。禁用邀请码、封禁用户或换码时必须主动删除缓存；不能只依赖 TTL。

## Outbox And Rewards

注册链路只写事实，不同步发奖励。后续奖励通过事件消费：

- `UserRegistered`
- `UserInvited`
- `UserInviteBecameValid`

`UserInvited` payload 至少包含：

```json
{
  "app_code": "lalu",
  "invited_user_id": "20001",
  "inviter_user_id": "10001",
  "invite_code": "A1B2C3",
  "source": "onboarding",
  "bound_at_ms": 1777996800000
}
```

`UserInviteBecameValid` payload 至少包含：

```json
{
  "app_code": "lalu",
  "invited_user_id": "20001",
  "inviter_user_id": "10001",
  "policy_id": "9001",
  "policy_version": "invite-valid-v1",
  "required_recharge_coin_amount": 80000,
  "accumulated_recharge_coin_amount": 80000,
  "valid_transaction_id": "wtx_01",
  "valid_at_ms": 1777996800000
}
```

奖励消费者必须幂等，幂等键使用 `app_code + invited_user_id + reward_type`，不能因为 outbox 重投重复发放。需要按有效邀请发奖励时，只消费 `UserInviteBecameValid`，不要消费原始 `UserInvited`。

## Risk Controls

- gateway 对带邀请码的注册请求继续使用 public auth 频控。
- user-service 记录 `device_id`、IP、UA、country_by_ip，用于后续识别同设备互邀、批量注册和异常渠道。
- 自己邀请自己必须拒绝。
- 同设备大量互邀、同 IP 大量注册、邀请人短时间异常增长不阻断注册主链路，但要进入风控统计或后台审核。
- 有效邀请达成前必须保留充值事件来源、充值类型和触发交易 ID，便于识别币商刷量、同设备互充、异常退款和人工充值滥用。
- 后台可以冻结某个邀请人的新邀请资格或奖励资格，但不能删除历史 `user_invite_relations`。
- 邀请码解析失败不要返回邀请人是否存在的细节；统一 `INVALID_INVITE_CODE`。

## Admin Requirements

后台需要提供最小管理能力：

- 查询用户自己的邀请码、邀请人数、有效邀请人数、被邀请用户列表和有效达成明细。
- 启用/禁用某个用户的邀请码；禁用只影响新绑定，不删除历史关系。
- 配置有效邀请策略：门槛金币数、适用区域、可计入充值类型、生效时间和停用时间。
- 查看某个被邀请用户的充值累计进度和触发有效邀请的钱包交易 ID。
- 标记异常邀请或撤销奖励资格；撤销奖励资格不能物理删除邀请关系和有效达成记录。
- 触发计数重算任务，用于修复 `user_invite_counters` 与事实表不一致。

所有后台写操作必须经过 `hyapp-admin-server` 鉴权和审计，不能让 App 用户直接调用。

## Implementation Sequence

1. 增加 `user_invite_codes` 和 `user_invite_relations` 表。
2. 增加 `user_invite_validity_policies`、`user_invite_recharge_progress`、`user_invite_event_consumption`、`user_invite_counters` 和 `user_outbox`。
3. 后台增加有效邀请策略配置，默认 `required_recharge_coin_amount=80000`。
4. user-service 新用户创建时生成自己的邀请码。
5. `LoginThirdParty` 首次创建用户时解析可选 `invite_code` 并绑定关系。
6. `CompleteOnboarding` 增加可选 `invite_code`，仅在未完成资料且未绑定关系时处理。
7. 绑定成功后写 `UserInvited`，并通过 outbox 幂等维护 `invite_count`。
8. 消费钱包 `WalletRechargeRecorded` 事件，累计被邀请用户充值金币，达到策略门槛后写 `UserInviteBecameValid` 并维护 `valid_invite_count`。
9. `GetUser` 或 `GetUserOverview` 增加当前用户的邀请码、邀请人数、有效邀请人数和当前有效门槛。
10. gateway HTTP request/response 增加字段映射。
11. 加测试和基础监控。

## Test Matrix

| Scenario | Expected |
| --- | --- |
| 新用户不填邀请码 | 注册成功，无 `user_invite_relations` |
| 新用户填有效邀请码 | 注册成功，关系表写入 inviter |
| 新用户填不存在邀请码 | 返回 `INVALID_INVITE_CODE`，不创建完成关系 |
| 邀请码跨 App | 返回 `INVALID_INVITE_CODE` |
| 邀请码跨区域 | 返回 `INVALID_INVITE_CODE` |
| 邀请人 disabled/banned | 返回 `INVALID_INVITE_CODE` |
| 用户尝试填自己的邀请码 | 返回 `INVALID_INVITE_CODE` |
| 已绑定关系后再次提交邀请码 | 原关系不变 |
| 已完成注册后提交邀请码 | 不修改原关系，不补填邀请人 |
| 三方身份再次登录带邀请码 | 登录成功但不修改邀请关系 |
| 并发提交两个邀请码 | 只有一条关系成功，另一条命中幂等或冲突 |
| 邀请码大小写/空格 | trim + uppercase 后解析 |
| 被邀请用户累计充值 79999 COIN | `invite_count` 已增加，`valid_invite_count` 不增加 |
| 被邀请用户累计充值达到 80000 COIN | 首次标记 `user_invite_recharge_progress.status=valid`，`valid_invite_count` 增加 1 |
| 被邀请用户一次充值超过门槛 | 只产生 1 个有效邀请 |
| 被邀请用户多次充值累计达到门槛 | 达标那笔交易触发有效邀请 |
| 同一充值事件重复消费 | 累计金额和有效邀请计数不重复增加 |
| 充值发生在绑定邀请码之前 | 默认不补算有效邀请，除非后台重算任务明确处理 |
| 后台把有效邀请门槛改为 100000 | 已经 valid 的历史事实不重算，新达成用户按新策略判断 |
| 我的页 overview | 返回当前用户自己的邀请码、`invite_count`、`valid_invite_count`，不实时扫关系或充值流水 |
