## ADDED Requirements

### Requirement: 换卡记录实体定义

系统 SHALL 定义换卡记录(CardReplacementRecord)实体，记录老卡到新卡的完整转移过程，包括套餐权益、代理关系、所有者信息等。

**核心概念**：
- **换卡场景**：老卡损坏、丢失或故障，需要更换新卡
- **权益转移**：老卡的套餐（含剩余流量）、代理关系、所有者信息等全部转移到新卡
- **套餐继续生效**：转移后套餐不作废，剩余流量继续可用

**实体字段**：
- `id`：换卡记录 ID（主键，BIGINT）
- `replacement_no`：换卡单号（VARCHAR(50)，唯一）
- `old_card_id`：老卡 ID（BIGINT，关联 tb_iot_card.id）
- `old_iccid`：老卡 ICCID（VARCHAR(50)，冗余存储，防止老卡被删除后无法追踪）
- `new_card_id`：新卡 ID（BIGINT，关联 tb_iot_card.id）
- `new_iccid`：新卡 ICCID（VARCHAR(50)，冗余存储）
- `old_owner_type`：老卡所有者类型（VARCHAR(20)）
- `old_owner_id`：老卡所有者 ID（BIGINT）
- `old_agent_id`：老卡代理 ID（BIGINT，可空）
- `new_owner_type`：新卡所有者类型（VARCHAR(20)）
- `new_owner_id`：新卡所有者 ID（BIGINT）
- `new_agent_id`：新卡代理 ID（BIGINT，可空）
- `package_snapshot`：套餐快照（JSONB，记录转移时的套餐详情）
- `replacement_reason`：换卡原因（VARCHAR(20)，枚举值："damaged"-损坏 | "lost"-丢失 | "malfunction"-故障 | "upgrade"-升级 | "other"-其他）
- `remark`：备注（TEXT）
- `status`：换卡状态（INT，1-待审批 2-已通过 3-已拒绝 4-已完成）
- `approved_by`：审批人 ID（BIGINT，可空）
- `approved_at`：审批时间（TIMESTAMP，可空）
- `completed_at`：完成时间（TIMESTAMP，可空）
- `creator`：创建人 ID（BIGINT）
- `updater`：更新人 ID（BIGINT）
- `created_at`：创建时间（TIMESTAMP，自动填充）
- `updated_at`：更新时间（TIMESTAMP，自动填充）
- `deleted_at`：删除时间（TIMESTAMP，可空，软删除）

**套餐快照 JSON 格式示例**：
```json
{
  "package_id": 3001,
  "package_name": "月套餐 10GB",
  "package_code": "PKG-M-001",
  "data_limit_mb": 10240,
  "data_usage_mb": 5120,
  "real_data_usage_mb": 4000,
  "virtual_data_usage_mb": 1120,
  "data_remaining_mb": 5120,
  "activated_at": "2026-01-01T00:00:00Z",
  "expires_at": "2026-02-01T00:00:00Z",
  "remaining_days": 15,
  "order_id": 10001
}
```

#### Scenario: 创建换卡记录

- **WHEN** 用户（ID 为 2001）的老卡（ICCID 为 "8986001"）损坏，需要换新卡（ICCID 为 "8986002"）
- **THEN** 系统创建换卡记录，`old_card_id` 为老卡 ID，`new_card_id` 为新卡 ID，`replacement_reason` 为 "damaged"，`status` 为 1（待审批）

#### Scenario: 审批通过换卡

- **WHEN** 运营人员（ID 为 999）审批通过换卡记录（ID 为 5001）
- **THEN** 系统将换卡记录状态从 1（待审批）变更为 2（已通过），记录 `approved_by` 为 999，`approved_at` 为当前时间

#### Scenario: 完成换卡

- **WHEN** 换卡记录（ID 为 5001）状态为 2（已通过），系统执行换卡操作
- **THEN** 系统将：
  1. 记录老卡和新卡的快照信息（所有者、代理、套餐）
  2. 将老卡的套餐权益转移到新卡（套餐使用记录的 `iot_card_id` 更新为新卡 ID）
  3. 将新卡的 `owner_type` 和 `owner_id` 更新为老卡的值
  4. 将新卡的代理关系更新为老卡的值（如有）
  5. 将换卡记录状态变更为 4（已完成），记录 `completed_at` 为当前时间

#### Scenario: 拒绝换卡

- **WHEN** 运营人员（ID 为 999）拒绝换卡记录（ID 为 5001），原因为"新卡不符合要求"
- **THEN** 系统将换卡记录状态从 1（待审批）变更为 3（已拒绝），记录 `approved_by` 为 999，`approved_at` 为当前时间，`remark` 为拒绝原因

---

### Requirement: 套餐权益转移

系统 SHALL 在换卡完成后，将老卡的套餐权益（包括剩余流量、过期时间等）转移到新卡，套餐继续生效。

**转移内容**：
- 套餐使用记录（`tb_package_usage`）
- 剩余流量（`data_limit_mb - data_usage_mb`）
- 套餐过期时间（`expires_at`）
- 关联的订单信息

**转移规则**：
- 老卡的套餐使用记录的 `iot_card_id` 更新为新卡 ID
- 剩余流量完整保留
- 套餐过期时间不变
- 如果老卡有多个套餐（正式套餐 + 加油包），全部转移

#### Scenario: 套餐转移

- **WHEN** 老卡有月套餐（剩余 5120 MB 流量，还有 15 天过期）
- **THEN** 系统将套餐使用记录的 `iot_card_id` 从老卡 ID 更新为新卡 ID，流量和过期时间保持不变

#### Scenario: 多套餐转移

- **WHEN** 老卡有正式套餐和 2 个加油包
- **THEN** 系统将所有套餐使用记录的 `iot_card_id` 更新为新卡 ID，所有套餐继续生效

---

### Requirement: 代理关系转移

系统 SHALL 在换卡完成后，将老卡的代理关系转移到新卡。

**转移内容**：
- 新卡的 `owner_type` 更新为老卡的 `owner_type`
- 新卡的 `owner_id` 更新为老卡的 `owner_id`
- 如果老卡通过代理销售，新卡继承相同的代理关系

#### Scenario: 代理关系转移

- **WHEN** 老卡的 `owner_type` 为 "agent"，`owner_id` 为 123
- **THEN** 系统将新卡的 `owner_type` 更新为 "agent"，`owner_id` 更新为 123

---

### Requirement: 换卡记录查询

系统 SHALL 支持按老卡 ID、新卡 ID、用户 ID、换卡单号等条件查询换卡记录。

**查询条件**：
- 换卡单号（精确匹配）
- 老卡 ID（精确匹配）
- 新卡 ID（精确匹配）
- 老卡 ICCID（精确匹配或模糊匹配）
- 新卡 ICCID（精确匹配或模糊匹配）
- 换卡状态（单选或多选）
- 换卡原因（单选或多选）
- 创建时间范围
- 完成时间范围

**分页**：
- 默认每页 20 条，最大每页 100 条
- 返回总记录数和总页数

#### Scenario: 按老卡 ICCID 查询换卡记录

- **WHEN** 查询老卡 ICCID 为 "8986001" 的换卡记录
- **THEN** 系统返回所有 `old_iccid` 为 "8986001" 的换卡记录列表

#### Scenario: 按状态查询换卡记录

- **WHEN** 查询状态为 1（待审批）的换卡记录
- **THEN** 系统返回所有 `status` 为 1 的换卡记录列表，按创建时间倒序排列

---

### Requirement: 换卡数据校验

系统 SHALL 对换卡数据进行校验，确保数据完整性和一致性。

**校验规则**：
- `old_card_id`：必填，≥ 1，必须是有效的 IoT 卡 ID
- `new_card_id`：必填，≥ 1，必须是有效的 IoT 卡 ID，不能与 `old_card_id` 相同
- `old_iccid`：必填，长度 19-20 字符
- `new_iccid`：必填，长度 19-20 字符，不能与 `old_iccid` 相同
- `replacement_reason`：必填，枚举值 "damaged" | "lost" | "malfunction" | "upgrade" | "other"
- `status`：必填，枚举值 1-4

#### Scenario: 换卡时老卡和新卡相同

- **WHEN** 创建换卡记录，`old_card_id` 和 `new_card_id` 都为 1001
- **THEN** 系统拒绝创建，返回错误信息"新卡不能与老卡相同"

#### Scenario: 换卡时新卡 ICCID 无效

- **WHEN** 创建换卡记录，`new_iccid` 长度为 15（小于 19）
- **THEN** 系统拒绝创建，返回错误信息"ICCID 长度必须为 19-20 字符"

#### Scenario: 换卡时老卡不存在

- **WHEN** 创建换卡记录，`old_card_id` 为 99999（不存在的 IoT 卡）
- **THEN** 系统拒绝创建，返回错误信息"老卡不存在"
