docs: 同步能力规范 — 新增购买/重置后复机 spec,更新停复机和排队激活规范
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m8s

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-03-31 18:12:52 +08:00
parent 8b5cd50576
commit ad095d73f3
4 changed files with 160 additions and 0 deletions

View File

@@ -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 张卡保持停机状态

View File

@@ -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** 单个复机失败不影响其他卡的处理

View File

@@ -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` 跳过,不会错误触发自动复机

View File

@@ -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`(已停用)