docs: 同步能力规范 — 新增购买/重置后复机 spec,更新停复机和排队激活规范
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m8s
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:
36
openspec/specs/auto-resume-on-purchase/spec.md
Normal file
36
openspec/specs/auto-resume-on-purchase/spec.md
Normal 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 张卡保持停机状态
|
||||
37
openspec/specs/auto-resume-on-reset/spec.md
Normal file
37
openspec/specs/auto-resume-on-reset/spec.md
Normal 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** 单个复机失败不影响其他卡的处理
|
||||
@@ -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` 跳过,不会错误触发自动复机
|
||||
|
||||
@@ -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`(已停用)
|
||||
|
||||
Reference in New Issue
Block a user