# 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