All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m54s
- 重构 Worker 启动流程,引入 bootstrap 模块统一管理依赖注入 - 实现套餐流量重置服务(日/月/年周期重置) - 新增套餐激活排队、加油包绑定、囤货待实名激活逻辑 - 新增订单创建幂等性防重(Redis 业务键 + 分布式锁) - 更新 AGENTS.md/CLAUDE.md:新增注释规范、幂等性规范,移除测试要求 - 添加套餐系统升级完整文档(API文档、使用指南、功能总结、运维指南) - 归档 OpenSpec package-system-upgrade 变更,同步 specs 到主目录 - 新增 queue types 抽象和 Redis 常量定义
421 lines
18 KiB
Markdown
421 lines
18 KiB
Markdown
# Spec: 流量扣减优先级机制
|
|
|
|
## 业务背景
|
|
|
|
现有套餐系统在流量扣减时不区分主套餐和加油包,导致:
|
|
1. **用户体验差**:用户购买加油包后,主套餐仍在扣减,加油包未生效
|
|
2. **停机逻辑错误**:主套餐流量用完即停机,加油包剩余流量浪费
|
|
3. **流量统计混乱**:多套餐同时扣减,无法追溯流量消耗路径
|
|
|
|
本规范引入流量扣减优先级机制,确保:
|
|
- **加油包优先扣减**:购买加油包后,优先消耗加油包流量
|
|
- **主套餐兜底**:加油包用完后,再扣减主套餐流量
|
|
- **全部用完停机**:主套餐 + 所有加油包流量都用完才停机
|
|
|
|
## 业务规则
|
|
|
|
### 扣减优先级规则(多维度排序)
|
|
```
|
|
优先级(从高到低):
|
|
1. 加油包(按 priority ASC, expires_at ASC, activated_at ASC)
|
|
2. 主套餐
|
|
```
|
|
|
|
**多维度排序规则**(按优先级递减):
|
|
1. **主键:priority ASC** - 数字越小优先级越高(1 > 2 > 3)
|
|
2. **次键:expires_at ASC** - 先到期的优先扣减(避免流量浪费)
|
|
3. **兜底:activated_at ASC** - 先激活的优先扣减(相同到期时间时)
|
|
|
|
**SQL 示例**:
|
|
```sql
|
|
SELECT * FROM tb_package_usage
|
|
WHERE card_id = ?
|
|
AND status = 'active'
|
|
AND remaining_data_amount > 0
|
|
ORDER BY
|
|
priority ASC, -- 加油包(priority=1)在正式套餐(priority=10)前
|
|
expires_at ASC, -- 同优先级:3天后到期的在7天后到期的前
|
|
activated_at ASC -- 同到期时间:早激活的在晚激活的前
|
|
LIMIT 10;
|
|
```
|
|
|
|
**业务意义**:
|
|
- **先用即将到期的**:避免流量过期浪费
|
|
- **确定性排序**:相同条件下结果稳定,便于问题排查
|
|
|
|
**示例**:
|
|
```
|
|
载体有:主套餐(剩余10GB) + 加油包A(priority=1, 剩余5GB) + 加油包B(priority=2, 剩余3GB)
|
|
产生 12GB 流量:
|
|
1. 扣减加油包A:5GB → 0GB(用完)
|
|
2. 扣减加油包B:3GB → 0GB(用完)
|
|
3. 扣减主套餐:4GB → 6GB(剩余6GB)
|
|
```
|
|
|
|
### 停机条件规则
|
|
- **旧逻辑**:主套餐流量用完即停机
|
|
- **新逻辑**:主套餐 + 所有加油包流量都用完才停机
|
|
|
|
**判断逻辑**:
|
|
```sql
|
|
SELECT COUNT(*) FROM tb_package_usage
|
|
WHERE (iot_card_id/device_id)=? AND status=1
|
|
AND data_usage_mb < data_limit_mb;
|
|
|
|
-- 如果 COUNT = 0,则触发停机
|
|
```
|
|
|
|
### 流量扣减算法
|
|
```
|
|
输入:上游返回的累计流量(upstream_cumulative_mb)
|
|
输出:更新各套餐的 data_usage_mb
|
|
|
|
1. 查询载体当前生效套餐(status=1),按优先级排序:
|
|
加油包(priority ASC)→ 主套餐
|
|
2. 计算本次流量增量:
|
|
increment = upstream_cumulative_mb - 上次记录的累计流量
|
|
3. 依次扣减:
|
|
FOR EACH 套餐 IN 优先级列表:
|
|
可扣减量 = MIN(increment, 套餐剩余额度)
|
|
UPDATE data_usage_mb += 可扣减量
|
|
记录到 PackageUsageDailyRecord
|
|
increment -= 可扣减量
|
|
IF data_usage_mb >= data_limit_mb:
|
|
UPDATE status=2(已用完)
|
|
IF increment == 0:
|
|
BREAK
|
|
4. 检查停机条件:
|
|
IF 所有套餐 status=2:
|
|
触发停机操作
|
|
```
|
|
|
|
### 并发控制
|
|
- **场景**:轮询系统同时检测到多张卡的流量增加
|
|
- **机制**:数据库事务 + 行锁(SELECT FOR UPDATE)
|
|
- **保证**:同一套餐不会被并发扣减导致负数流量
|
|
|
|
### 性能要求
|
|
- 单次流量扣减 < 100ms(包含数据库更新 + 日记录写入)
|
|
- 批量扣减(1000张卡) < 10秒
|
|
|
|
## Requirements
|
|
|
|
### Requirement: 流量优先扣减加油包
|
|
系统 SHALL 在扣减流量时,优先扣减加油包流量,再扣减主套餐流量。
|
|
|
|
**业务价值**:用户购买加油包后,立即生效,优先消耗加油包流量,避免浪费。
|
|
|
|
**技术实现**:
|
|
- 查询时按 `master_usage_id IS NOT NULL, priority ASC` 排序
|
|
- 主套餐(master_usage_id=NULL)排在最后
|
|
|
|
#### Scenario: 存在加油包时优先扣减
|
|
- **GIVEN** 载体有主套餐(data_usage_mb=0, data_limit_mb=10240)和加油包(data_usage_mb=0, data_limit_mb=5120, priority=1)
|
|
- **WHEN** 上游返回累计流量 3072MB(本次增量 3GB)
|
|
- **THEN** 系统执行:
|
|
1. 扣减加油包:data_usage_mb=3072
|
|
2. 主套餐不扣减:data_usage_mb=0
|
|
- **AND** PackageUsageDailyRecord 记录加油包增量 3072MB
|
|
|
|
#### Scenario: 加油包用完后扣减主套餐
|
|
- **GIVEN** 载体有主套餐(data_usage_mb=0, data_limit_mb=10240)和加油包(data_usage_mb=3072, data_limit_mb=5120)
|
|
- **WHEN** 上游返回累计流量 8192MB(本次增量 5GB)
|
|
- **THEN** 系统执行:
|
|
1. 扣减加油包:5120 - 3072 = 2048MB 可用,扣减 2048MB → data_usage_mb=5120(用完)
|
|
2. 更新加油包 status=2(已用完)
|
|
3. 剩余流量 5GB - 2GB = 3GB
|
|
4. 扣减主套餐:data_usage_mb=3072
|
|
- **AND** PackageUsageDailyRecord 记录加油包增量 2048MB、主套餐增量 3072MB
|
|
|
|
#### Scenario: 只有主套餐时直接扣减
|
|
- **GIVEN** 载体只有主套餐(data_usage_mb=0, data_limit_mb=10240),无加油包
|
|
- **WHEN** 上游返回累计流量 3072MB
|
|
- **THEN** 系统直接扣减主套餐:data_usage_mb=3072
|
|
- **AND** PackageUsageDailyRecord 记录主套餐增量 3072MB
|
|
|
|
#### Scenario: 加油包已用完自动跳过(边界条件)
|
|
- **GIVEN** 载体有主套餐(data_usage_mb=0, data_limit_mb=10240)和加油包(data_usage_mb=5120, data_limit_mb=5120, status=2)
|
|
- **WHEN** 上游返回累计流量 3072MB
|
|
- **THEN** 系统跳过已用完的加油包,直接扣减主套餐:data_usage_mb=3072
|
|
- **AND** 加油包 data_usage_mb 保持 5120(不再扣减)
|
|
|
|
#### Scenario: 流量增量为 0 不扣减(边界条件)
|
|
- **GIVEN** 载体有主套餐和加油包
|
|
- **WHEN** 上游返回累计流量与上次记录相同(增量=0)
|
|
- **THEN** 系统不更新任何套餐的 data_usage_mb
|
|
- **AND** 不创建 PackageUsageDailyRecord
|
|
|
|
#### Scenario: 流量增量为负数拒绝扣减(异常处理)
|
|
- **GIVEN** 载体上次记录累计流量 10GB
|
|
- **WHEN** 上游返回累计流量 8GB(负增量,异常情况)
|
|
- **THEN** 系统记录 Warning 日志:"上游流量异常,累计流量减少"
|
|
- **AND** 不更新套餐 data_usage_mb
|
|
- **AND** 告警通知运维团队
|
|
|
|
### Requirement: 多个加油包按多维度排序扣减
|
|
|
|
系统 SHALL 当存在多个加油包时,按 **priority ASC, expires_at ASC, activated_at ASC** 多维度排序扣减流量。
|
|
|
|
**业务价值**:
|
|
- 按购买顺序消耗加油包(priority)
|
|
- 优先消耗即将到期的流量(expires_at)
|
|
- 确定性排序便于问题排查(activated_at)
|
|
|
|
**技术实现**:
|
|
- 查询时:`ORDER BY (master_usage_id IS NOT NULL) DESC, priority ASC, expires_at ASC, activated_at ASC`
|
|
- 确保加油包按多维度排序排在主套餐前
|
|
|
|
#### Scenario: 按到期时间优先扣减(多维度排序验证)
|
|
- **GIVEN** 载体有2个加油包,相同 priority:
|
|
- 加油包A:priority=1, data_limit_mb=5120, expires_at=2026-02-15 23:59:59
|
|
- 加油包B:priority=1, data_limit_mb=3072, expires_at=2026-02-12 23:59:59(先到期)
|
|
- **WHEN** 上游返回累计流量 4096MB(本次增量 4GB)
|
|
- **THEN** 系统执行:
|
|
1. 扣减加油包B(先到期):3072MB → data_usage_mb=3072(用完),status=2
|
|
2. 剩余流量 4GB - 3GB = 1GB
|
|
3. 扣减加油包A:1024MB → data_usage_mb=1024
|
|
- **AND** PackageUsageDailyRecord 记录加油包B增量 3072MB、加油包A增量 1024MB
|
|
|
|
#### Scenario: 完整多维度排序示例
|
|
- **GIVEN** 载体有:
|
|
- 主套餐:priority=10, data_limit_mb=10240, expires_at=2026-03-31
|
|
- 加油包A:priority=1, data_limit_mb=2048, expires_at=2026-02-15, activated_at=2026-02-01
|
|
- 加油包B:priority=2, data_limit_mb=3072, expires_at=2026-02-20, activated_at=2026-02-03
|
|
- 加油包C:priority=1, data_limit_mb=4096, expires_at=2026-02-15, activated_at=2026-02-05(与A同priority和expires_at,但晚激活)
|
|
- **WHEN** 上游返回累计流量 12288MB(本次增量 12GB)
|
|
- **THEN** 系统按以下顺序扣减:
|
|
1. 加油包A(priority=1, expires_at=2026-02-15, activated_at=2026-02-01 最早)
|
|
2. 加油包C(priority=1, expires_at=2026-02-15, activated_at=2026-02-05)
|
|
3. 加油包B(priority=2)
|
|
4. 主套餐(priority=10)
|
|
- **AND** 扣减结果:
|
|
- 加油包A:2048MB → status=2(用完)
|
|
- 加油包C:4096MB → status=2(用完)
|
|
- 加油包B:3072MB → status=2(用完)
|
|
- 主套餐:3072MB(剩余 12GB - 2GB - 4GB - 3GB)
|
|
|
|
#### Scenario: 按购买顺序扣减多个加油包
|
|
- **GIVEN** 载体有加油包A(priority=1, data_usage_mb=0, data_limit_mb=3072)和加油包B(priority=2, data_usage_mb=0, data_limit_mb=5120)
|
|
- **WHEN** 上游返回累计流量 4096MB(本次增量 4GB)
|
|
- **THEN** 系统执行:
|
|
1. 扣减加油包A:3072MB → data_usage_mb=3072(用完),status=2
|
|
2. 剩余流量 4GB - 3GB = 1GB
|
|
3. 扣减加油包B:1024MB → data_usage_mb=1024
|
|
- **AND** PackageUsageDailyRecord 记录加油包A增量 3072MB、加油包B增量 1024MB
|
|
|
|
#### Scenario: Priority 最小的加油包用完后扣减下一个
|
|
- **GIVEN** 载体有3个加油包(priority=1/2/3),priority=1 已用完(status=2)
|
|
- **WHEN** 上游返回累计流量增量 2GB
|
|
- **THEN** 系统跳过 priority=1,扣减 priority=2 的加油包 2GB
|
|
|
|
#### Scenario: 所有加油包用完后扣减主套餐
|
|
- **GIVEN** 载体有主套餐和2个加油包(priority=1/2),两个加油包都已用完(status=2)
|
|
- **WHEN** 上游返回累计流量增量 5GB
|
|
- **THEN** 系统跳过所有加油包,扣减主套餐 5GB
|
|
|
|
#### Scenario: 3个加油包和主套餐的完整扣减流程
|
|
- **GIVEN** 载体有:
|
|
- 主套餐(data_limit_mb=10240, data_usage_mb=0)
|
|
- 加油包A(priority=1, data_limit_mb=2048, data_usage_mb=0)
|
|
- 加油包B(priority=2, data_limit_mb=3072, data_usage_mb=0)
|
|
- 加油包C(priority=3, data_limit_mb=4096, data_usage_mb=0)
|
|
- **WHEN** 上游返回累计流量 12288MB(本次增量 12GB)
|
|
- **THEN** 系统执行:
|
|
1. 扣减加油包A:2048MB → status=2(用完)
|
|
2. 扣减加油包B:3072MB → status=2(用完)
|
|
3. 扣减加油包C:4096MB → status=2(用完)
|
|
4. 扣减主套餐:3072MB(剩余 12GB - 2GB - 3GB - 4GB)
|
|
- **AND** PackageUsageDailyRecord 记录 4 条记录
|
|
|
|
#### Scenario: 并发扣减同一套餐(并发控制)
|
|
- **GIVEN** 两个轮询任务同时检测到同一张卡的流量增加
|
|
- **WHEN** 两个任务同时尝试扣减加油包A
|
|
- **THEN** 第一个任务获取行锁(SELECT FOR UPDATE),执行扣减
|
|
- **AND** 第二个任务等待锁释放,检测到已扣减,跳过(幂等性保证)
|
|
- **AND** 加油包A的 data_usage_mb 只增加一次
|
|
|
|
### Requirement: 所有流量用完时触发停机
|
|
系统 SHALL 在主套餐和所有加油包流量都用完时,触发停机操作。
|
|
|
|
**业务价值**:充分利用加油包流量,避免提前停机,提升用户体验。
|
|
|
|
**技术实现**:
|
|
```sql
|
|
-- 停机条件检查
|
|
SELECT COUNT(*) FROM tb_package_usage
|
|
WHERE (iot_card_id/device_id)=? AND status=1 AND master_usage_id IS NULL;
|
|
|
|
-- 如果 COUNT=0(主套餐已过期或用完),检查加油包
|
|
SELECT COUNT(*) FROM tb_package_usage
|
|
WHERE (iot_card_id/device_id)=? AND status=1 AND master_usage_id IS NOT NULL
|
|
AND data_usage_mb < data_limit_mb;
|
|
|
|
-- 如果两个 COUNT 都=0,触发停机
|
|
```
|
|
|
|
#### Scenario: 主套餐和加油包都用完触发停机
|
|
- **GIVEN** 主套餐 data_usage_mb=10240, data_limit_mb=10240(用完),加油包 data_usage_mb=5120, data_limit_mb=5120(用完)
|
|
- **WHEN** 轮询系统检查停机条件
|
|
- **THEN** 系统查询生效中套餐剩余流量,结果为 0
|
|
- **AND** 触发停机操作:
|
|
1. 调用运营商 API 停机
|
|
2. 更新 IotCard.network_status=0(已停机)
|
|
3. 记录操作日志
|
|
- **AND** 主套餐和加油包 status 更新为 2(已用完)
|
|
|
|
#### Scenario: 有加油包剩余流量时不停机
|
|
- **GIVEN** 主套餐 data_usage_mb=10240, data_limit_mb=10240(用完),加油包 data_usage_mb=4096, data_limit_mb=5120(剩余1GB)
|
|
- **WHEN** 轮询系统检查停机条件
|
|
- **THEN** 系统查询生效中套餐剩余流量,结果 > 0
|
|
- **AND** 不触发停机,继续提供服务
|
|
|
|
#### Scenario: 主套餐未用完但加油包都用完(不停机)
|
|
- **GIVEN** 主套餐 data_usage_mb=8192, data_limit_mb=10240(剩余2GB),所有加油包都用完(status=2)
|
|
- **WHEN** 轮询系统检查停机条件
|
|
- **THEN** 系统查询主套餐剩余流量 > 0
|
|
- **AND** 不触发停机
|
|
|
|
#### Scenario: 主套餐过期但加油包有剩余(不停机)
|
|
- **GIVEN** 主套餐 status=3(已过期),加油包 data_usage_mb=2048, data_limit_mb=5120(剩余3GB), status=1
|
|
- **WHEN** 轮询系统检查停机条件
|
|
- **THEN** 系统查询生效中加油包剩余流量 > 0
|
|
- **AND** 不触发停机
|
|
|
|
#### Scenario: 停机后续费加油包自动复机(业务理解)
|
|
- **GIVEN** 载体已停机(所有套餐流量用完)
|
|
- **WHEN** 用户购买新加油包(立即激活,status=1)
|
|
- **THEN** 下次轮询检查时,发现有剩余流量 > 0
|
|
- **AND** 自动触发复机操作:
|
|
1. 调用运营商 API 复机
|
|
2. 更新 IotCard.network_status=1(已开机)
|
|
3. 记录操作日志
|
|
|
|
#### Scenario: 停机 API 调用失败(异常处理)
|
|
- **GIVEN** 载体所有套餐流量用完,需要停机
|
|
- **WHEN** 调用运营商停机 API 失败(例如网络超时)
|
|
- **THEN** 系统记录 Error 日志,包含卡号、错误信息
|
|
- **AND** 停机任务进入重试队列(Asynq 重试 3 次,间隔 10 秒)
|
|
- **AND** 如果 3 次重试都失败,进入死信队列(DLQ)
|
|
- **AND** 告警通知运维团队
|
|
|
|
### Requirement: 流量扣减记录到日记录表
|
|
系统 SHALL 在扣减流量时,更新 PackageUsage 的 data_usage_mb,并创建或更新 PackageUsageDailyRecord。
|
|
|
|
**业务价值**:
|
|
- 精细化流量统计(按套餐、按日)
|
|
- 支持流量详单查询
|
|
- 数据可追溯、可审计
|
|
|
|
**技术实现**:
|
|
- 扣减流量后,创建或更新当日 PackageUsageDailyRecord
|
|
- 使用 UPSERT(ON CONFLICT UPDATE)避免重复记录
|
|
- 记录字段:`package_usage_id`, `date`, `daily_usage_mb`, `cumulative_usage_mb`
|
|
|
|
#### Scenario: 扣减主套餐流量并记录
|
|
- **GIVEN** 主套餐 data_usage_mb=0, data_limit_mb=10240
|
|
- **WHEN** 扣减主套餐 2048MB 流量
|
|
- **THEN** PackageUsage 更新:data_usage_mb=2048
|
|
- **AND** PackageUsageDailyRecord 创建记录:
|
|
- package_usage_id=主套餐ID
|
|
- date=2026-02-10
|
|
- daily_usage_mb=2048
|
|
- cumulative_usage_mb=2048
|
|
|
|
#### Scenario: 扣减加油包流量并记录
|
|
- **GIVEN** 加油包 data_usage_mb=0, data_limit_mb=5120
|
|
- **WHEN** 扣减加油包 3072MB 流量
|
|
- **THEN** PackageUsage 更新:data_usage_mb=3072
|
|
- **AND** PackageUsageDailyRecord 创建记录:
|
|
- package_usage_id=加油包ID
|
|
- date=2026-02-10
|
|
- daily_usage_mb=3072
|
|
- cumulative_usage_mb=3072
|
|
|
|
#### Scenario: 同一天多次扣减更新日记录
|
|
- **GIVEN** PackageUsageDailyRecord 已有记录(date=2026-02-10, daily_usage_mb=2048, cumulative_usage_mb=2048)
|
|
- **WHEN** 再次扣减主套餐 1024MB 流量
|
|
- **THEN** PackageUsage 更新:data_usage_mb=3072
|
|
- **AND** PackageUsageDailyRecord 更新记录:
|
|
- daily_usage_mb=3072(2048 + 1024)
|
|
- cumulative_usage_mb=3072
|
|
- **AND** 使用 UPSERT 更新而非插入新记录
|
|
|
|
#### Scenario: 跨天扣减创建新日记录
|
|
- **GIVEN** PackageUsageDailyRecord 有 2026-02-10 的记录(daily_usage_mb=5120, cumulative_usage_mb=5120)
|
|
- **WHEN** 2026-02-11 扣减主套餐 2048MB 流量
|
|
- **THEN** PackageUsageDailyRecord 创建新记录:
|
|
- date=2026-02-11
|
|
- daily_usage_mb=2048
|
|
- cumulative_usage_mb=7168(5120 + 2048)
|
|
|
|
#### Scenario: 日记录写入失败不影响扣减(容错性)
|
|
- **GIVEN** 数据库主表正常,日记录表存在问题(例如磁盘满)
|
|
- **WHEN** 扣减主套餐流量,PackageUsage 更新成功,但 PackageUsageDailyRecord 写入失败
|
|
- **THEN** 系统记录 Error 日志,包含套餐ID、日期、增量
|
|
- **AND** PackageUsage 的 data_usage_mb 仍然更新(不回滚)
|
|
- **AND** 告警通知运维团队修复日记录表
|
|
|
|
#### Scenario: 批量扣减写入日记录(性能优化)
|
|
- **GIVEN** 轮询系统同时检测到 1000 张卡的流量增加
|
|
- **WHEN** 批量扣减流量
|
|
- **THEN** 使用批量 INSERT ON CONFLICT UPDATE 写入日记录
|
|
- **AND** 1000 条记录写入时间 < 5 秒
|
|
|
|
## 数据一致性保证
|
|
|
|
### 1. 扣减流量事务保证
|
|
- **机制**:数据库事务包含:
|
|
1. UPDATE PackageUsage SET data_usage_mb += increment
|
|
2. INSERT/UPDATE PackageUsageDailyRecord
|
|
- **回滚条件**:任一步骤失败,整个事务回滚
|
|
|
|
### 2. 并发扣减行锁
|
|
- **机制**:`SELECT * FROM tb_package_usage WHERE id=? FOR UPDATE`
|
|
- **保证**:同一套餐不会被并发扣减
|
|
|
|
### 3. 负数流量保护
|
|
- **机制**:数据库约束 `CHECK (data_usage_mb >= 0)`
|
|
- **保证**:扣减后不会出现负数流量
|
|
|
|
### 4. 日记录唯一索引
|
|
- **机制**:`UNIQUE INDEX (package_usage_id, date) WHERE deleted_at IS NULL`
|
|
- **保证**:同一套餐同一天只有一条记录
|
|
|
|
## 性能指标
|
|
|
|
| 操作 | 性能要求 | 监控指标 |
|
|
|------|---------|---------|
|
|
| 单次流量扣减 | < 100ms | 数据库事务耗时 |
|
|
| 批量扣减(1000张卡) | < 10秒 | 轮询任务执行时间 |
|
|
| 日记录写入 | < 50ms | INSERT/UPDATE 耗时 |
|
|
| 停机条件检查 | < 50ms | SELECT 查询耗时 |
|
|
|
|
## 错误码定义
|
|
|
|
| 错误码 | HTTP 状态码 | 错误消息 | 场景 |
|
|
|--------|------------|---------|------|
|
|
| CodeInternal | 500 | 流量扣减失败,请重试 | 数据库更新失败 |
|
|
| CodeInternal | 500 | 停机操作失败,请重试 | 运营商 API 调用失败 |
|
|
|
|
## 测试场景矩阵
|
|
|
|
| 维度 | 场景 | 预期结果 |
|
|
|------|------|---------|
|
|
| **基础扣减** | 只有主套餐 | 直接扣减主套餐 |
|
|
| | 有1个加油包 | 优先扣减加油包 |
|
|
| | 有3个加油包 | 按 priority 顺序扣减 |
|
|
| **扣减完整流程** | 加油包用完 → 主套餐 | 先扣完所有加油包,再扣主套餐 |
|
|
| | 所有套餐用完 | 触发停机 |
|
|
| **边界条件** | 流量增量=0 | 不扣减 |
|
|
| | 流量增量<0(异常) | 拒绝扣减,告警 |
|
|
| | 加油包已用完 | 自动跳过 |
|
|
| **并发场景** | 并发扣减同一套餐 | 行锁保证只扣减一次 |
|
|
| **停机条件** | 主套餐用完+加油包剩余 | 不停机 |
|
|
| | 所有套餐用完 | 停机 |
|
|
| | 停机后购买加油包 | 自动复机 |
|
|
| **日记录** | 首次扣减 | 创建日记录 |
|
|
| | 同一天多次扣减 | 更新日记录 |
|
|
| | 跨天扣减 | 创建新日记录 |
|
|
| **异常处理** | 停机 API 失败 | 重试 3 次,失败进 DLQ |
|
|
| | 日记录写入失败 | 告警,不影响扣减 |
|