# Spec: 时间字段 Nullable 修复

## 背景

多个 Model 将语义上可为空的时间字段定义为非指针 `time.Time`，导致 GORM 在未赋值时将 Go 零值（`0001-01-01 00:00:00`）写入数据库。受影响字段跨 4 个 Model、5 个字段，其中 `tb_package_usage` 的两个字段在 DB 层也是 NOT NULL，需同步迁移。

## ADDED Requirements

### Requirement: PackageUsage 时间字段可为 NULL

`PackageUsage.ActivatedAt` 和 `ExpiresAt` SHALL 定义为 `*time.Time`，数据库列 `activated_at` 和 `expires_at` SHALL 允许 NULL。

当套餐处于待生效状态（`status=0`）时，`activated_at` 和 `expires_at` SHALL 为 NULL，不得写入零值。

#### Scenario: 创建排队套餐时时间字段为 NULL

- **WHEN** 购买主套餐时已有生效中主套餐，新套餐被创建为 status=0（待生效）
- **THEN** `tb_package_usage` 中该记录的 `activated_at` 和 `expires_at` 均为 NULL
- **AND** 不写入 `0001-01-01` 或任何零值时间

#### Scenario: 套餐激活后时间字段被正确赋值

- **WHEN** 待生效套餐（status=0）被成功激活
- **THEN** `activated_at` 被更新为激活时刻的实际时间
- **AND** `expires_at` 被更新为根据套餐 CalendarType/Duration 计算出的到期时间
- **AND** 两个字段均不为 NULL

#### Scenario: 历史零值数据被清洗

- **WHEN** 数据库迁移执行完成
- **THEN** `tb_package_usage` 中所有 `status=0` 且 `activated_at < '2000-01-01'` 的记录，其 `activated_at` 和 `expires_at` 均被更新为 NULL

---

### Requirement: 个人客户绑定时间字段可为 NULL

`PersonalCustomerDevice.LastUsedAt`、`PersonalCustomerICCID.LastUsedAt`、`PersonalCustomerPhone.VerifiedAt` SHALL 定义为 `*time.Time`。

设备/ICCID 首次绑定时 `LastUsedAt` SHALL 为 NULL（尚未使用）。手机号绑定未经验证时 `VerifiedAt` SHALL 为 NULL。

#### Scenario: 设备首次绑定时 LastUsedAt 为 NULL

- **WHEN** 个人客户首次绑定设备（`personal_customer_device` 记录被创建）
- **THEN** `last_used_at` 字段为 NULL
- **AND** 不写入 `0001-01-01` 零值

#### Scenario: ICCID 首次绑定时 LastUsedAt 为 NULL

- **WHEN** 个人客户首次绑定 ICCID（`personal_customer_iccid` 记录被创建）
- **THEN** `last_used_at` 字段为 NULL

#### Scenario: 手机号绑定但未验证时 VerifiedAt 为 NULL

- **WHEN** 个人客户绑定手机号但尚未完成验证流程
- **THEN** `tb_personal_customer_phone.verified_at` 字段为 NULL

---

### Requirement: 全量编译无类型错误

Model 改为指针类型后，所有引用 `ActivatedAt`、`ExpiresAt`、`LastUsedAt`、`VerifiedAt` 的代码 SHALL 正确处理指针（nil 判断或解引用），`go build ./...` 必须零错误通过。

#### Scenario: 读取 ActivatedAt 时安全处理 nil

- **WHEN** 代码读取 `PackageUsage.ActivatedAt`（如展示给前端、计算剩余时长）
- **THEN** 必须先判断指针是否为 nil，nil 时使用零值时间或跳过该字段，不得直接解引用导致 panic
