From ad095d73f325eb8c26a51aab00b5861ea6b512a3 Mon Sep 17 00:00:00 2001 From: huang Date: Tue, 31 Mar 2026 18:12:52 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E5=90=8C=E6=AD=A5=E8=83=BD=E5=8A=9B?= =?UTF-8?q?=E8=A7=84=E8=8C=83=20=E2=80=94=20=E6=96=B0=E5=A2=9E=E8=B4=AD?= =?UTF-8?q?=E4=B9=B0/=E9=87=8D=E7=BD=AE=E5=90=8E=E5=A4=8D=E6=9C=BA=20spec?= =?UTF-8?q?=EF=BC=8C=E6=9B=B4=E6=96=B0=E5=81=9C=E5=A4=8D=E6=9C=BA=E5=92=8C?= =?UTF-8?q?=E6=8E=92=E9=98=9F=E6=BF=80=E6=B4=BB=E8=A7=84=E8=8C=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- .../specs/auto-resume-on-purchase/spec.md | 36 ++++++++++++ openspec/specs/auto-resume-on-reset/spec.md | 37 ++++++++++++ openspec/specs/auto-stop-resume/spec.md | 57 +++++++++++++++++++ .../specs/package-queue-activation/spec.md | 30 ++++++++++ 4 files changed, 160 insertions(+) create mode 100644 openspec/specs/auto-resume-on-purchase/spec.md create mode 100644 openspec/specs/auto-resume-on-reset/spec.md diff --git a/openspec/specs/auto-resume-on-purchase/spec.md b/openspec/specs/auto-resume-on-purchase/spec.md new file mode 100644 index 0000000..89ca462 --- /dev/null +++ b/openspec/specs/auto-resume-on-purchase/spec.md @@ -0,0 +1,36 @@ +## ADDED Requirements + +### Requirement: 购买套餐支付成功后自动复机 + +系统 SHALL 在支付回调成功激活套餐(`PackageUsage.status` 变为 1)后,对因流量耗尽而停机的卡自动触发复机操作。 + +#### Scenario: C 端购买套餐支付成功后停机卡自动复机 +- **GIVEN** 卡 C1 因流量耗尽停机(`network_status=0`,`stop_reason=traffic_exhausted`),已完成实名认证(`real_name_status=1`) +- **WHEN** C 端用户为 C1 购买新套餐并完成微信/支付宝支付,`HandlePaymentCallback` 事务提交成功 +- **THEN** 系统异步调用 `ResumeCardIfStopped(ctx, "iot_card", cardID)` +- **AND** 系统调用 `gateway.StartCard`,更新 `network_status=1`,清空 `stop_reason` + +#### Scenario: 后台购买套餐(钱包支付)后停机卡自动复机 +- **GIVEN** 卡 C2 因流量耗尽停机,已实名 +- **WHEN** 后台管理员通过钱包支付为 C2 购买套餐,`HandlePaymentCallback` 事务提交成功,套餐状态为 `status=1` +- **THEN** 系统调用复机逻辑,C2 自动复机 + +#### Scenario: 购买排队套餐(新套餐为 Pending 状态)时不触发复机 +- **GIVEN** 卡 C3 已有生效中套餐,用户购买第二个套餐(新套餐创建为 `status=0` Pending) +- **WHEN** 支付成功,`HandlePaymentCallback` 完成 +- **THEN** 系统 SHALL NOT 触发复机调用(仅在套餐变为 `status=1` 时才复机) + +#### Scenario: 未实名卡购买套餐不触发复机 +- **GIVEN** 卡 C4 停机(`stop_reason=traffic_exhausted`),但 `real_name_status=0`(未实名) +- **WHEN** 为 C4 购买新套餐且套餐激活为 `status=1` +- **THEN** `ResumeCardIfStopped` 内部检测到未实名,静默跳过,C4 保持停机状态 + +#### Scenario: 停机原因非流量耗尽时不触发复机 +- **GIVEN** 卡 C5 因手动停机(`stop_reason=manual`) +- **WHEN** 为 C5 购买新套餐并激活 +- **THEN** `ResumeCardIfStopped` 检测到 `stop_reason` 不为 `traffic_exhausted`,静默跳过 + +#### Scenario: 设备类型载体购买套餐后自动复机 +- **GIVEN** 设备 D1 绑定了 3 张卡,其中 2 张已实名且因流量耗尽停机,1 张未实名 +- **WHEN** 为 D1 购买设备级套餐并激活 +- **THEN** 已实名的 2 张卡自动复机,未实名的 1 张卡保持停机状态 diff --git a/openspec/specs/auto-resume-on-reset/spec.md b/openspec/specs/auto-resume-on-reset/spec.md new file mode 100644 index 0000000..8f893dc --- /dev/null +++ b/openspec/specs/auto-resume-on-reset/spec.md @@ -0,0 +1,37 @@ +## ADDED Requirements + +### Requirement: 流量周期重置后自动复机 + +系统 SHALL 在成功重置套餐流量(`data_usage_mb` 归零,`status` 恢复为 `Active`)后,对因流量耗尽而停机的卡自动触发复机操作。 + +#### Scenario: 3 个月套餐第 2 个月耗尽停机,第 3 个月重置后自动复机 +- **GIVEN** 卡 C1 有一个 3 个月套餐(`data_reset_cycle=monthly`),第 2 个月流量耗尽(`status=2 Depleted`),卡已停机(`network_status=0`,`stop_reason=traffic_exhausted`),已实名 +- **WHEN** 月度重置任务运行,套餐 `data_usage_mb` 归零,`status` 恢复为 `1 Active` +- **THEN** `ResetService` 调用 `ResumeCardIfStopped(ctx, "iot_card", cardID)` +- **AND** 卡自动复机(`network_status=1`) + +#### Scenario: 日流量套餐重置后停机卡自动复机 +- **GIVEN** 卡 C2 有日流量套餐(`data_reset_cycle=daily`),当天流量耗尽停机,已实名 +- **WHEN** 次日零点日流量重置任务运行 +- **THEN** 卡自动复机 + +#### Scenario: 年流量套餐重置后停机卡自动复机 +- **GIVEN** 卡 C3 有年流量套餐(`data_reset_cycle=yearly`),年内流量耗尽停机,已实名 +- **WHEN** 年度重置任务运行 +- **THEN** 卡自动复机 + +#### Scenario: 重置后卡已处于开机状态不重复复机 +- **GIVEN** 卡 C4 套餐被重置,但 `network_status=1`(已开机,可能已被人工复机) +- **WHEN** `ResumeCardIfStopped` 被调用 +- **THEN** 检测到卡已开机,幂等跳过,不调用 Gateway + +#### Scenario: 重置后未实名卡不复机 +- **GIVEN** 卡 C5 套餐被重置,`real_name_status=0`(未实名),停机中 +- **WHEN** `ResumeCardIfStopped` 被调用 +- **THEN** 检测到未实名,静默跳过,卡保持停机 + +#### Scenario: 批量重置中部分套餐的卡已停机 +- **GIVEN** 月度重置扫描到 100 个套餐,其中 10 个对应的卡处于停机状态且已实名 +- **WHEN** 月度重置完成 +- **THEN** 10 张停机卡异步触发复机,其余 90 张正常跳过 +- **AND** 单个复机失败不影响其他卡的处理 diff --git a/openspec/specs/auto-stop-resume/spec.md b/openspec/specs/auto-stop-resume/spec.md index 5b3ac8a..2b88402 100644 --- a/openspec/specs/auto-stop-resume/spec.md +++ b/openspec/specs/auto-stop-resume/spec.md @@ -389,3 +389,60 @@ func (s *Service) ResumeCardIfStopped(ctx context.Context, cardID uint) error { - ✅ 并发场景和异常处理 - ✅ 性能指标和错误码定义 - ✅ 测试场景矩阵和实现参考 + +--- + +## 迭代更新(fix-stop-resume-lifecycle-engine) + +### MODIFIED Requirement: 流量耗尽自动复机 + +系统 SHALL 在套餐激活或流量重置后,对满足以下全部条件的卡自动调用运营商接口复机: +1. `stop_reason = "traffic_exhausted"`(仅限流量耗尽停机,手动停机不自动复机) +2. `real_name_status = 1`(已完成实名认证) +3. `network_status = 0`(当前处于停机状态) + +系统 SHALL 在执行复机前获取 Redis 分布式锁(Key:`card:resume:lock:{cardID}`,TTL 30s),防止并发重复调用 Gateway。 + +#### Scenario: 所有条件满足时自动复机 +- **GIVEN** 卡 C1 满足:`stop_reason=traffic_exhausted`,`real_name_status=1`,`network_status=0` +- **WHEN** `ResumeCardIfStopped(ctx, "iot_card", cardID)` 被调用 +- **THEN** 系统获取分布式锁,调用 `gateway.StartCard`,更新 `network_status=1`,清空 `stop_reason`,释放锁 + +#### Scenario: 未实名卡跳过自动复机 +- **GIVEN** 卡 C2 `stop_reason=traffic_exhausted`,`real_name_status=0`,`network_status=0` +- **WHEN** `ResumeCardIfStopped` 被调用 +- **THEN** 系统记录 WARN 日志,静默跳过,不调用 Gateway,函数返回 nil + +#### Scenario: 手动停机的卡不被自动复机 +- **GIVEN** 卡 C3 `stop_reason=manual`,`real_name_status=1`,`network_status=0` +- **WHEN** `ResumeCardIfStopped` 被调用 +- **THEN** 系统检测到 `stop_reason != traffic_exhausted`,静默跳过 + +#### Scenario: 并发调用时只有一次复机执行 +- **GIVEN** 卡 C4 满足复机条件,复机操作正在执行(分布式锁被持有) +- **WHEN** 第二个 `ResumeCardIfStopped` 调用同时到达 +- **THEN** 第二个调用获锁失败,直接返回,不调用 Gateway + +#### Scenario: 设备类型载体复机遍历所有绑定卡 +- **GIVEN** 设备 D1 绑定 3 张卡(C1 已实名停机、C2 已实名停机、C3 未实名停机) +- **WHEN** `ResumeCardIfStopped(ctx, "device", deviceID)` 被调用 +- **THEN** C1 和 C2 自动复机,C3 因未实名跳过 + +### ADDED Requirement: 套餐超额停机必须写入 stop_reason 和 stopped_at + +系统 SHALL 在套餐虚流量超额触发停机时(`stopCards` 函数),写入 `stop_reason=traffic_exhausted` 和 `stopped_at` 到 IoT 卡记录。 + +#### Scenario: 虚流量超额停机完整写入 DB +- **GIVEN** 卡 C1 当月流量超过套餐 `virtual_data_mb` 上限,`network_status=1` +- **WHEN** `HandlePackageCheck` 的 `stopCards` 执行 +- **THEN** Gateway 调用成功后,DB 更新包含:`network_status=0`,`stop_reason=traffic_exhausted`,`stopped_at=now()`,`updated_at=now()` + +### ADDED Requirement: 保护期停机使用常量 stop_reason + +系统 SHALL 在保护期一致性检查停机时,使用 `constants.StopReasonProtectPeriod` 常量(值:`protect_period`)记录 `stop_reason`。 + +#### Scenario: 保护期停机写入正确原因 +- **GIVEN** 设备处于停机保护期,绑定的卡 C1 `network_status=1`(开机状态不一致) +- **WHEN** `HandleProtectConsistencyCheck` 触发停机 +- **THEN** DB 中 `stop_reason=protect_period`(使用常量,非中文硬编码) +- **AND** `ResumeCardIfStopped` 调用时因 `stop_reason != traffic_exhausted` 跳过,不会错误触发自动复机 diff --git a/openspec/specs/package-queue-activation/spec.md b/openspec/specs/package-queue-activation/spec.md index e69b330..f6404eb 100644 --- a/openspec/specs/package-queue-activation/spec.md +++ b/openspec/specs/package-queue-activation/spec.md @@ -381,3 +381,33 @@ CREATE INDEX idx_package_usage_priority ON tb_package_usage(iot_card_id, status, | | 轮询系统宕机 | 恢复后自动激活过期套餐 | | **性能场景** | 单个套餐激活延迟 | < 20 秒 | | | 10000 个套餐同时过期 | P99 < 5 分钟 | +| **Depleted 场景** | 已耗尽套餐到期 + 有排队套餐 | 耗尽套餐→Expired,激活下一个 | +| | 已耗尽套餐到期 + 无排队套餐 | 耗尽套餐→Expired,载体进入无套餐状态 | + +--- + +## 迭代更新(fix-stop-resume-lifecycle-engine) + +### MODIFIED Requirement: 同时只能有一个生效中的主套餐 + +**到期检测扩展**:过期检测 SHALL 同时覆盖 `status=1`(生效中)和 `status=2`(已耗尽)的主套餐。当 `expires_at <= now` 时,无论套餐当前是生效中还是已耗尽,均视为到期,触发下一个排队套餐激活。 + +查询条件:`WHERE status IN (1,2) AND expires_at <= now AND master_usage_id IS NULL` + +#### Scenario: 已耗尽(Depleted)主套餐到期后触发下一个激活 +- **GIVEN** 卡 C2 有已耗尽主套餐(`status=2 Depleted`,`expires_at=过去时间`)和一个预购排队套餐(`status=0`) +- **WHEN** 过期检测任务扫描(查询条件:`status IN (1,2) AND expires_at <= now AND master_usage_id IS NULL`) +- **THEN** 耗尽套餐更新为 `status=3 Expired`,加油包级联失效,预购排队套餐激活为 `status=1` +- **AND** 如卡因流量耗尽停机且已实名,触发自动复机 + +#### Scenario: 已耗尽套餐到期时加油包级联失效 +- **GIVEN** 卡 C3 有耗尽主套餐(`status=2`,`expires_at=过去时间`),以及绑定到该主套餐的 1 个加油包(`status=1`,尚有剩余流量) +- **WHEN** 过期检测任务处理该主套餐 +- **THEN** 主套餐更新为 `status=3`,加油包更新为 `status=4 Invalidated`(即使尚有剩余流量) +- **AND** 下一个排队主套餐激活 + +#### Scenario: 已耗尽套餐到期且无下一个排队套餐 +- **GIVEN** 卡 C4 有耗尽主套餐(`status=2`,`expires_at=过去时间`),无排队套餐 +- **WHEN** 过期检测任务处理该主套餐 +- **THEN** 耗尽套餐更新为 `status=3`,加油包级联失效,无新套餐激活 +- **AND** 卡的业务状态更新为 `IotCardStatusSuspended=4`(已停用)