feat: 卡流量读数先过采集队列再入账,扣减按采集时刻确定套餐
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m27s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m27s
- 每卡 Redis 采集队列(默认 N=3、跨天清空、回退容差 1 MB),攒满且不下降才写最后一个点 - 测试环境全量切换日快照计量,下线灰度名单、切换水位与卡表基线扣费;全部卡置待建起点 - 读数区分没给值与 0;有套餐的卡读数 0 也写快照与日用量 0 行;重置日比较基准为 0 照常防旧值 - 新增采集记录(按月分区保留 90 天)、人工冲正流水与后台接口、采集告警 - 扣减按采集时刻有效套餐查找,只对生效中套餐做状态迁移,查不到套餐按成功跳过 - 换货新卡(新设备下的卡)打待建起点;采集间隔上限只校验启用中的轮询配置 Change: add-card-traffic-queue-and-observed-deduction Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-10-29
|
||||
@@ -0,0 +1,79 @@
|
||||
## Context
|
||||
|
||||
完整推导、例子与决策编号(T1–T25)见 `docs/卡流量采集写入与扣减技术方案.md`。本文只记录实现层的关键决策。
|
||||
|
||||
现状(测试环境,`Iteration/8-11`):
|
||||
|
||||
- `rebuild-card-traffic-metering-and-stop-resume` 已实现日快照(`tb_card_flow_day`)、自然月换算与重置日防旧值(`internal/domain/cardflow/rules.go`)、按月对账、切换水位与灰度名单(`traffic_calc_policy.go`);测试环境 `JUNHONG_TRAFFIC_CALC_MODE=gray`、名单 `device:10`。
|
||||
- `cardflow.ValidReading` 要求读数 > 0,测试库全部轮询卡读数为 0,`tb_card_flow_day` 与 `tb_card_flow_switch` 均为 0 行,扣费实际一直走卡表基线算法。
|
||||
- `internal/gateway/models.go` 的 `FlexFloat` 把 null、空、无法解析都转成 0。
|
||||
- `DeductDataUsage`(`internal/service/package/usage_service.go`)按扣费时刻 `status = 生效中` 查套餐,没有套餐时返回错误由 Asynq 重试;`updateDailyRecord` 先查后插、日期取 `time.Now()`;循环内会把余量 ≤ 0 的套餐改为已用完。
|
||||
- `hasActivePackageForCard` / `countActivePackages`(`traffic_calc_policy.go:135-171`)只认生效中、不看 `expires_at`。
|
||||
- 测试环境启用中的轮询配置只有 id 29(流量采集 60 秒),Redis 为 `noeviction`、未设 `maxmemory`,数据库与容器时区为上海;只有实名回调,没有运营商流量推送。
|
||||
- 约束:自动化测试为 N/A,验证在维护者指定的测试环境进行;不改既有迁移;不改对接日志与审计写入(本次不动)。
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- 单次异常读数(中间点暴涨、清零前后来回跳)在写入前拦下,不进快照、不扣费、不触发通道阈值。
|
||||
- 测试环境只剩一条计量路径,切换不重复扣。
|
||||
- 采集侧与扣费侧"有没有套餐"一致,套餐用完到停机之间的用量能扣到。
|
||||
- 每次采集可追溯,扣错可人工冲正。
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- 自动对账(运营商 T+1 日用量或月账单)、月末尾巴补算。
|
||||
- 最后一个点暴涨、运营商连续返回递增错误值、运营商晚清零的自动识别。
|
||||
- 跨套餐重置时由写入间隔造成的用量归属偏移(避免不了,不处理)。
|
||||
- 对接日志、审计的瘦身;复机无效重试问题。
|
||||
- 生产环境发布、迁移与回滚。
|
||||
|
||||
## Decisions
|
||||
|
||||
**D1 队列放 Redis、Lua 原子更新,不放数据库。** 队列只是写入前的过滤器,不是账:丢了只是晚写 N−1 次,不多扣不少扣。放数据库会让每次采集都多一次行写,与 `polling-load-control` 的低写入目标冲突。Lua 内只做插入、去重、裁剪、跨天清空与过期丢弃,返回队列与"新点是否在队尾";上海日期由 Go 侧算好随点传入,Lua 不做时区换算。写入判断(不下降 + 容差)在 Go 侧做,便于记录原因。
|
||||
|
||||
**D2 写最后一个点,新点必须在队尾。** 写最后一个点让增量最及时;新点不在队尾说明有更晚的点已判断过,再写会让采集记录看起来写了自己的读数,排查时误导,所以记为乱序不写。容差只用于"不下降"判断,日快照取最大值、增量 = 最大值 − 已结算,小幅回退的点写进去不产生增量,不会多扣。替代方案"写倒数第二个点"能挡住最后一个点暴涨,但每次写入都晚一个采集周期,已否决。
|
||||
|
||||
**D3 采集间隔上限按 N+2 次。** 理论最低是窗口内采到 N 次,但一次失败当天就写不进去,告警会常态触发;留 2 次冗余。只校验启用中的轮询配置,停用配置(测试库 id 25,86400 秒)不阻止保存,启用时再校验。
|
||||
|
||||
**D4 全量切换用"待建起点"标记,不保留切换水位。** 切换前扣费走卡表基线算法,日快照上没有本月已结算;不打标记,切换后第一次写入的 Δ 会是整月用量,而这部分老路径已扣过。标记与换卡共用同一字段(`tb_iot_card` 新增),随迁移把全部卡置位;建起点也过队列,避免异常读数当起点。迁移执行时老代码仍在跑也安全:老代码不认标记照常按老路径扣,新代码第一次写入只建起点。代价是老路径最后一次扣费到建起点之间的用量不扣(约 N−1 个采集周期),测试环境可接受。
|
||||
|
||||
**D5 "有套餐"抽成一个共用判断,采集与扣费同时调用。** 由 package 服务提供按 `(卡, 采集时间)` 查有效套餐的方法;观测事务(写入范围、是否发事件、是否写采集记录、日用量 0 行)与 `DeductDataUsage` 都调用它。状态纳入已过期,是因为状态可能在采集之后才变;是否在采集时刻有效由 `expires_at` 判断。
|
||||
|
||||
**D6 读数 0 由写入范围决定写不写,不由读数校验拦。** `ValidReading` 放宽为"有限且 ≥ 0";有套餐的卡 0 也写快照与日用量 0 行,用于"每天都有记录"的排查需求;没套餐的卡读数 0 不写。`isStale` 对 ≤ 0 直接返回否,换算结果不变。
|
||||
|
||||
**D7 重置日照搬老系统,只认配置的重置日;比较基准为 0 统一照常防旧值。** 老系统移动(`YidongDayFlowService`)、联通(`PtLiantongDayFlowService`)都是重置日当天全天 0.9 判断、不按小时、尾巴不补。两套代码对基准为 0 的处理不同(移动照常判断、联通不判断),统一为照常判断:`isStale` 去掉 `ThisMonthBeforeResetMax > 0` 条件,最坏是晚一天扣,不会多扣。不设多日警惕期、不把 1 点前的读数记回前一天。
|
||||
|
||||
**D8 日用量 0 行在观测事务里写,扣减消费者只累加。** 消费者只在 Δ > 0 时运行,没用流量的卡走不到那里。观测事务调用 package 服务的"确保当天日用量行"(`INSERT ... ON CONFLICT (package_usage_id, date) DO NOTHING`),不在 cardobservation 里直接写套餐表;`updateDailyRecord` 改为 upsert 累加。
|
||||
|
||||
**D9 扣费查不到套餐按成功跳过。** 原来返回错误会让 Asynq 一直重试,而退款、作废后不可能再查到;改为写跳过审计、事件成功结束,需要时人工冲正。
|
||||
|
||||
**D10 采集记录单独建表、按月分区,不复用对接日志或审计。** 只记有套餐的卡;保留 90 天需要同时存在 4 个月分区,过期直接删整月分区,不用 `DELETE ... LIMIT`。`observation_id` 串起采集记录、流量事件、扣费审计。不进 Audit Event 与 Integration Log,与 `polling-load-control` 的"无变化观测不写审计"不冲突。
|
||||
|
||||
**D11 冲正锁套餐行、按当前值加减,提交后走停复机判断。** 与扣减同一把行锁,避免并发互相覆盖;不允许负数,避免掩盖数据问题。卡可能同时因实名、沉默期、人工等原因停着,冲正只改了"套餐用完"一个条件,交给 `evaluator.EvaluateAndAct` 综合判断。冲正不改日快照与已结算。
|
||||
|
||||
**D12 日期统一上海日期。** 套餐日用量日期与消费者日流量缓冲 key 都用 `cardflow.ShanghaiDate(observed_at)`;测试环境时区本就是上海,这是防止时间值为 UTC 时 0~8 点落到前一天。
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [最后一个点暴涨挡不住] → 靠人工冲正;告警与采集记录便于发现。
|
||||
- [运营商晚一天清零、清零后返回缓存旧数会多扣] → 与老系统一致;重置日防旧值命中数用于核对,多扣走冲正。
|
||||
- [要攒满 N 个点才写,扣费与停机延后;跨天清空开启时当天第一次写入在第一次采集后 (N−1) 个间隔] → 测试环境间隔 60 秒,影响约 2 分钟;需要更及时时缩短间隔。
|
||||
- [全量切换、换卡后到建起点之间的用量不扣] → 约 N−1 个采集周期,测试环境可接受。
|
||||
- [采集后套餐被重置的扣在重置后周期] → 审计记录采集时间与重置时间,可追溯。
|
||||
- [采集后退款、作废的用量不扣] → 跳过审计 + 人工冲正。
|
||||
- [采集记录写入量 ≈ 有套餐卡数 × 每天采集次数,测试环境 60 秒间隔下单卡每天 1440 行] → 测试环境卡少;分区按月删除。
|
||||
- [Redis 内存随卡数增长,每卡约 150 字节] → 测试环境 `noeviction` 无上限,随卡数观察。
|
||||
- [`DeductDataUsage` 查询范围扩大后,旧循环会把已过期套餐改成已用完] → 状态迁移只作用于当前生效中套餐(规格已明确)。
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. 前置:维护者确认后归档 `rebuild-card-traffic-metering-and-stop-resume`(灰度、切换水位相关验证 9.2–9.5 作废,停复机部分已由 `finalize-unified-stop-resume-rule` 覆盖)与 `add-tiered-polling-and-pre-activation-settlement`,使 `card-traffic-metering` 主规格存在。
|
||||
2. 迁移(成对):采集记录分区表、冲正流水表、`tb_iot_card` 待建起点标记(迁移内对全部卡置位)、套餐日用量 `(package_usage_id, date)` 唯一索引(建前核对并合并重复行)。
|
||||
3. 注册系统配置项默认值;删除计量模式与灰度名单配置及测试环境部署变量。
|
||||
4. 部署测试环境后按 tasks 验证;回滚只在测试环境:回退代码与迁移 down,队列 key 自然过期。
|
||||
|
||||
## Open Questions
|
||||
|
||||
- 重置日读取失败时仍按运营商类型兜底(`CUCC` 取 27、其余取 1,`rebuild-...` 的 D6)。这与"只看配置的重置日"不完全一致,本 Change 保持不改,是否改为"读取失败本次不写入并告警"待定。
|
||||
@@ -0,0 +1,38 @@
|
||||
## Why
|
||||
|
||||
`rebuild-card-traffic-metering-and-stop-resume` 已把计量改为日快照 + 本月最大值 − 已结算,解决了基线冻结、重置窗口重复扣的问题,但运营商单次异常读数仍直接写进快照(当天取最大值,暴涨会留下来);测试环境仍在灰度与卡表基线算法两条路径并存;扣减按扣费时刻的"生效中"套餐查找,采集与扣费对"有没有套餐"口径不一致,套餐用完到停机之间的用量收不到;扣错没有冲正通道,排查也缺少逐次采集记录。依据为 `docs/卡流量采集写入与扣减技术方案.md`(2026-10-28 修订,决策 T1–T25 及 10-28/29 补充决定)。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 运营商流量读数先判有效性:接口失败、限流、缺字段、`used` 为空/null/无法解析、负数、NaN/Inf 不入队、不写快照;有效读数为有限且 ≥ 0,Gateway 流量读数区分"没值"与 0。
|
||||
- 新增每卡 Redis 采集队列(默认 N = 3,Lua 原子):按采集时间插入,跨天清空(可配,默认开)、丢弃过期点;新点在队尾、队列满 N 个点且相邻两点回退不超过容差(默认 1 MB)时才写**最后一个点**,否则不写并记录原因。轮询、人工刷新、激活前结算共用同一队列与规则;没有运营商流量推送。
|
||||
- 队列配置(长度、跨天清空、最长点龄、回退容差)放系统配置,改了即生效;启用中的轮询配置流量采集间隔超过上限(开关开 24h ÷ (N+2),关 `max_point_age_hours` ÷ (N+2))时拒绝保存或启用,停用的配置不校验。
|
||||
- **BREAKING**(测试环境)一次性全量切换:所有卡走日快照 + 本月最大值 − 已结算;删除计量模式与灰度名单、第一次切换观测与切换水位,卡表基线算法不再产生扣费增量。迁移时全部卡打"待建起点"标记,切换后第一次写入只建起点不扣费。
|
||||
- 写入范围:有套餐的卡读数为 0 也写日快照,每次采集写采集记录,每个有效套餐每天确保一行日用量(没扣写 0);没套餐的卡读数 > 0 才写快照,增量只记已结算、之后买套餐不补扣。
|
||||
- 重置日照搬老系统,只认通道配置的重置日:重置日当天全天读数 > 比较基准 × 0.9 按 0 计;比较基准为 0 时不论重置日是几号都照常按 0 计;次日起不再判断;月末尾巴不补、不记回前一天。
|
||||
- "有套餐"采集侧与扣费侧共用同一判断:采集时刻状态为生效中/已用完/已过期、已激活、未到期、未退款的套餐,卡自己没有时看绑定设备;已用完但在有效期内算有套餐。
|
||||
- 扣减按采集时刻确定套餐:只对当前"生效中"的套餐做状态迁移;一个套餐都查不到时记审计并按成功跳过;采集后套餐被重置的扣在重置后的周期;套餐日用量改为 upsert,日期取采集时间的上海日期(含消费者日流量缓冲 key)。
|
||||
- 换卡完成时新卡(设备换货时新设备下全部卡)打"待建起点"标记,第一次通过队列写入时建起点。
|
||||
- 新增采集记录表(有套餐的卡每次采集一条,按月分区保留 90 天)与后台查询;新增人工冲正流水(锁套餐行、退回后不得为负、提交后走停复机判断)与后台接口。
|
||||
- 新增告警:开启轮询且有套餐的卡一整天没写入、待建起点超过 1 天、队列拦截率异常、乱序突增、重置日防旧值命中数。
|
||||
- 不在本 Change:自动对账(运营商 T+1 数据)、月末尾巴补算、对接日志/审计瘦身、跨套餐重置时的用量偏移、生产环境发布与回滚。
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `package-traffic-deduction`:按采集时刻确定扣减套餐、状态迁移范围、查不到套餐的处理、套餐日用量记录、人工冲正。
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `card-traffic-metering`:读数有效性与快照写入范围(读数 0 写入)、重置日防旧值比较基准为 0 的处理、增量对全部卡生效且"有套餐"改为共用口径、通道阈值只在写入点判断、展示对全部卡生效、激活前结算过队列;新增采集队列、队列配置与采集间隔校验、待建起点(全量切换与换卡)、采集记录、告警;删除切换水位与按扣减载体灰度切换。
|
||||
|
||||
## Impact
|
||||
|
||||
- 前置:`rebuild-card-traffic-metering-and-stop-resume` 与 `add-tiered-polling-and-pre-activation-settlement` 须先归档(本 Change 修改它们新增的 `card-traffic-metering` 要求);前者剩余的灰度、切换水位验证作废。
|
||||
- 数据库:新增采集记录表(按月分区)、冲正流水表;`tb_iot_card` 增加待建起点标记,迁移时全部卡置位;套餐日用量 `(package_usage_id, date)` 唯一索引;`tb_card_flow_switch` 不再读写。均为新的成对迁移。
|
||||
- 代码:`internal/gateway/models.go`、`internal/task/polling_carddata_handler.go`、新增队列组件与系统配置项、`internal/application/cardobservation/`(`traffic.go`、`traffic_flow.go`、`traffic_calc_policy.go`)、`internal/domain/cardflow/rules.go`、`internal/domain/cardobservation/traffic.go`、`internal/infrastructure/cardobservation/event.go`、`internal/service/package/usage_service.go`、`internal/service/exchange`、人工刷新与激活前结算入口、轮询配置保存。
|
||||
- 接口:人工刷新返回本次读数、是否写入与原因;新增采集记录查询、冲正接口;重新生成接口文档。
|
||||
- 配置:删除 `JUNHONG_TRAFFIC_CALC_MODE`、`JUNHONG_TRAFFIC_CALC_CARDS`;新增系统配置 `traffic.queue.size`、`traffic.queue.clear_on_new_day`、`traffic.queue.max_point_age_hours`、`traffic.queue.drop_tolerance_mb`。
|
||||
- Redis:每卡一个队列 key(约 150 字节);测试环境为 `noeviction`。
|
||||
- 范围:测试环境。
|
||||
@@ -0,0 +1,263 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 流量读数有效性
|
||||
系统 SHALL 在运营商流量查询返回后先判断读数是否有效:接口失败、超时、非 200、限流、响应缺少 ICCID 等必要字段、`used` 为空或 null 或无法解析、读数为负数、NaN 或 Inf 的,均为无效读数,MUST NOT 进入采集队列、MUST NOT 写日快照、MUST NOT 产生增量。读数为有限且大于等于 0 的数时为有效读数。系统 MUST 区分运营商"没有给值"与"读数为 0",响应中完全缺少 `used` 字段时按没有给值处理。
|
||||
|
||||
#### Scenario: 运营商返回 null
|
||||
- **WHEN** 运营商流量查询成功,但 `used` 为 null
|
||||
- **THEN** 系统不入队、不写快照、不产生增量;卡有套餐时采集记录的结果为"无读数"、读数为空
|
||||
|
||||
#### Scenario: 运营商返回 0
|
||||
- **WHEN** 运营商流量查询成功,`used` 为 0
|
||||
- **THEN** 系统把 0 作为有效读数入队
|
||||
|
||||
#### Scenario: 响应缺少 used 字段
|
||||
- **WHEN** 运营商流量查询成功,但响应中没有 `used` 字段
|
||||
- **THEN** 系统按没有给值处理,不把它当成 0
|
||||
|
||||
### Requirement: 采集队列与写入判断
|
||||
系统 SHALL 为每张卡维护一个最近 N 个有效读数的采集队列(N 默认 3、最小 2),每个点为采集时间与运营商原始读数。新点 MUST 按采集时间插入队列,同一采集时间的点去重,只保留最后 N 个;队列更新 MUST 原子执行。同时满足以下条件时,系统 SHALL 把新点(即队列最后一个点)作为本次写入点进入日快照与结算:新点插入后位于队尾、队列满 N 个点、相邻两点后一个读数大于等于前一个读数减回退容差(默认 1 MB)。任一条件不满足时本次 MUST NOT 写快照、MUST NOT 产生增量、MUST NOT 做通道阈值判定,并记录不写原因(队列未满、读数下降、乱序)。轮询、人工刷新与激活前结算 MUST 使用同一队列与同一规则。队列丢失时系统 SHALL 从空队列重新开始,不补写。
|
||||
|
||||
#### Scenario: 中间点暴涨
|
||||
- **WHEN** 队列为 100、99999、120
|
||||
- **THEN** 系统不写入,原因为读数下降
|
||||
|
||||
#### Scenario: 持续上涨
|
||||
- **WHEN** 队列为 100、150、200,200 为新点
|
||||
- **THEN** 系统写入 200
|
||||
|
||||
#### Scenario: 持平
|
||||
- **WHEN** 有套餐的卡队列为 100、100、100
|
||||
- **THEN** 系统写入 100,增量为 0
|
||||
|
||||
#### Scenario: 回退在容差内
|
||||
- **WHEN** 回退容差为 1 MB,队列为 100.002、100.001、100.003
|
||||
- **THEN** 系统写入 100.003
|
||||
|
||||
#### Scenario: 运营商清零
|
||||
- **WHEN** 队列为 3010、20、25
|
||||
- **THEN** 系统不写入;之后队列为 20、25、30 时写入 30
|
||||
|
||||
#### Scenario: 迟到的采集点
|
||||
- **WHEN** 新点的采集时间早于队列中已有的点,插入后不在队尾
|
||||
- **THEN** 系统不写入,原因为乱序
|
||||
|
||||
#### Scenario: 队列丢失
|
||||
- **WHEN** 卡的队列因 Redis 重启丢失
|
||||
- **THEN** 系统从空队列开始,攒满 N 个点后照常写入,不多扣也不少扣
|
||||
|
||||
### Requirement: 人工刷新返回写入结果
|
||||
系统 SHALL 在人工刷新卡流量后返回本次运营商读数、是否写入以及不写入的原因(读数无效、队列未满、读数下降、乱序)。
|
||||
|
||||
#### Scenario: 人工刷新被队列拦下
|
||||
- **WHEN** 管理员人工刷新卡流量,本次读数使队列未满
|
||||
- **THEN** 接口返回本次读数、未写入、原因为队列未满
|
||||
|
||||
### Requirement: 跨天清空与过期点
|
||||
系统 SHALL 提供跨天清空开关(默认开启):开启时新点与队列中的点不在同一上海日期,先清空队列再入队。系统 SHALL 在入队时丢弃与新点采集时间相差超过最长点龄(默认 72 小时,0 表示不限)的点。
|
||||
|
||||
#### Scenario: 跨天
|
||||
- **WHEN** 跨天清空开启,队列中是前一天的点,新点是今天的
|
||||
- **THEN** 系统清空队列后再放入新点,今天攒满 N 个点后才写入
|
||||
|
||||
#### Scenario: 卡停用几天后重新采集
|
||||
- **WHEN** 跨天清空关闭,队列中最新的点是 4 天前的
|
||||
- **THEN** 系统丢弃这些点,只用新采集的点判断
|
||||
|
||||
### Requirement: 采集队列配置与采集间隔校验
|
||||
系统 SHALL 在系统配置中提供队列长度、跨天清空开关、最长点龄、回退容差四项配置,修改后无需重新发布即生效。系统 SHALL 只对启用中的轮询配置校验流量采集间隔:跨天清空开启时间隔 MUST 不超过 24 小时 ÷ (N+2),关闭时 MUST 不超过最长点龄 ÷ (N+2)。保存启用状态的轮询配置、启用轮询配置时间隔超限的 MUST 拒绝;修改队列长度或跨天清空开关会使任一启用中的轮询配置超限时 MUST 拒绝修改。停用的轮询配置 MUST NOT 被校验或阻止保存。
|
||||
|
||||
#### Scenario: 启用配置间隔过长
|
||||
- **WHEN** N = 3、跨天清空开启,维护者保存流量采集间隔为 6 小时的启用中轮询配置
|
||||
- **THEN** 系统拒绝保存并说明上限为 4.8 小时
|
||||
|
||||
#### Scenario: 停用配置不校验
|
||||
- **WHEN** 维护者保存流量采集间隔为 86400 秒的停用轮询配置
|
||||
- **THEN** 系统正常保存;启用该配置时按上限校验
|
||||
|
||||
#### Scenario: 调大队列长度使启用配置超限
|
||||
- **WHEN** 启用中的轮询配置间隔为 4 小时,维护者把队列长度从 3 改为 4
|
||||
- **THEN** 系统拒绝修改,因为上限变为 4 小时以下
|
||||
|
||||
### Requirement: 有套餐判断口径
|
||||
系统 SHALL 用同一个判断确定"采集时刻卡是否有套餐",供写入范围、是否发扣费事件、是否写采集记录与扣费时查找套餐共用:套餐状态为生效中、已用完或已过期,激活时间不晚于采集时间(激活时间为空的历史数据按已生效),到期时间为空或晚于采集时间,未退款;卡自身一个都没有时看卡当前绑定设备的套餐。待生效、已失效的套餐 MUST NOT 算有套餐。
|
||||
|
||||
#### Scenario: 已用完但仍在有效期
|
||||
- **WHEN** 卡唯一的套餐已用完,到期时间晚于采集时间
|
||||
- **THEN** 系统判定有套餐,增量照常发扣费事件
|
||||
|
||||
#### Scenario: 采集时套餐尚未激活
|
||||
- **WHEN** 采集时间早于套餐激活时间
|
||||
- **THEN** 系统判定没有套餐,增量只记已结算
|
||||
|
||||
#### Scenario: 沉默期等待中的套餐
|
||||
- **WHEN** 卡只有处于待生效状态的套餐
|
||||
- **THEN** 系统判定没有套餐
|
||||
|
||||
### Requirement: 待建起点
|
||||
系统 SHALL 在卡上维护"待建起点"标记。带标记的卡第一次满足队列写入条件时,系统 MUST 写日快照、把该卡本月最大快照全部记为已结算、不发扣费事件、清除标记,并在采集记录中记为建起点;之后按正常规则处理。以下情况 SHALL 置位:全量切换迁移时全部卡;换卡完成时的新卡;设备换货时新设备当前绑定的每张卡。换卡流程内 MUST NOT 调用运营商接口。
|
||||
|
||||
#### Scenario: 换卡后建起点
|
||||
- **WHEN** 新卡出库前已用 50 MB,换卡完成后依次采到 52、53、55、58
|
||||
- **THEN** 系统在写入 55 时建起点、不扣费,写入 58 时扣 3 MB
|
||||
|
||||
#### Scenario: 起点读数异常
|
||||
- **WHEN** 带标记的新卡第一次采到 99999,之后为 52、53
|
||||
- **THEN** 队列不满足写入条件,系统不以 99999 建起点
|
||||
|
||||
#### Scenario: 全量切换不重复扣
|
||||
- **WHEN** 全量切换后某卡第一次满足写入条件,本月老路径已扣过 800 MB
|
||||
- **THEN** 系统只建起点、不扣费,之后按增量正常扣
|
||||
|
||||
### Requirement: 采集记录
|
||||
系统 SHALL 为采集时刻有套餐的卡每次采集写一条采集记录,包括接口失败与读数为 0,记录采集时间、采集 ID、来源、读数结果(有效、失败、限流、响应无效、无读数、读数无效)、运营商原始读数、入队后的队列、写入判断(已写入、建起点、队列未满、读数下降、乱序、激活前结算用已有快照),写入时记录换算后的本月累计、是否触发防旧值、本次增量与流量事件 ID。没套餐的卡 MUST NOT 写采集记录。采集记录 SHALL 按采集时间按月分区,保留 90 天,过期按整月分区删除。系统 SHALL 提供后台按卡与时间范围查询采集记录的接口。采集 ID MUST 随流量事件与扣费审计传递,使扣费可追溯到对应采集记录。
|
||||
|
||||
#### Scenario: 被队列拦下的采集
|
||||
- **WHEN** 有套餐的卡本次读数使队列出现下降
|
||||
- **THEN** 系统写一条采集记录,判断为读数下降,记录队列内容,不写快照
|
||||
|
||||
#### Scenario: 从扣费追到采集
|
||||
- **WHEN** 维护者从一笔扣费审计排查
|
||||
- **THEN** 可经流量事件的采集 ID 找到对应采集记录中的原始读数与队列
|
||||
|
||||
#### Scenario: 没套餐的卡
|
||||
- **WHEN** 没套餐的卡被采集
|
||||
- **THEN** 系统不写采集记录
|
||||
|
||||
### Requirement: 流量采集告警
|
||||
系统 SHALL 对以下情况产生告警:开启轮询且有套餐的卡一整天没有写入;开启轮询的卡待建起点标记超过 1 天未清除;单卡当天读数下降拦截占比过高或全局拦截率突增;乱序数量突增。系统 SHALL 统计重置日防旧值命中数供核对重置日配置。未开启轮询的卡 MUST NOT 触发上述告警。当天没有写入时系统 MUST NOT 补写日快照。
|
||||
|
||||
#### Scenario: 有套餐的卡全天未写入
|
||||
- **WHEN** 开启轮询、有套餐的卡当天采集记录中没有已写入
|
||||
- **THEN** 系统产生告警,不补写当天快照
|
||||
|
||||
#### Scenario: 不轮询的卡
|
||||
- **WHEN** 未开启轮询的卡一直带待建起点标记
|
||||
- **THEN** 系统不告警
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 卡日快照记录
|
||||
系统 SHALL 为每张卡按上海日期每天保存一条"本月自然月累计流量"快照,同时记录当天最后一次写入的运营商原始读数、当天已结算增量合计、当天是否触发重置日防旧值和写入次数。快照只由通过采集队列判断的写入点写入,无效读数与被队列拦下的读数 MUST NOT 写入快照、MUST NOT 产生增量。写入点读数为 0 时,采集时刻有套餐的卡 MUST 写入快照,没套餐的卡 MUST NOT 写入;读数大于 0 时都写入。同一卡同一天多次写入时,本月累计 MUST 取已有值与本次换算值中的较大者,不得变小。快照写入 MUST 为原子写入,轮询、人工刷新与激活前结算并发写同一卡同一天时不得丢失较大值。快照永久保留。
|
||||
|
||||
#### Scenario: 同一天读到偏小的旧缓存
|
||||
- **WHEN** 某卡当天已有本月累计 500 MB 的快照,本次写入点换算值为 480 MB
|
||||
- **THEN** 当天快照的本月累计保持 500 MB,不产生负增量
|
||||
|
||||
#### Scenario: 有套餐的卡读数为 0
|
||||
- **WHEN** 有套餐的卡写入点读数为 0
|
||||
- **THEN** 系统写入当天快照,增量为 0
|
||||
|
||||
#### Scenario: 没套餐的卡读数为 0
|
||||
- **WHEN** 没套餐的卡写入点读数为 0
|
||||
- **THEN** 系统不写快照
|
||||
|
||||
#### Scenario: 读数无效
|
||||
- **WHEN** 运营商返回的已用流量为空、null、无法解析或为负数
|
||||
- **THEN** 系统不入队、不写快照、不产生增量
|
||||
|
||||
### Requirement: 重置日防旧值
|
||||
系统 SHALL 只在卡所属通道配置的重置日当天判断旧值,当天全天判断、不按小时区分:`R = 1` 时比较基准为上月最大快照,`R > 1` 时比较基准为本月 1~R−1 号最大快照;读数超过比较基准的 0.9 倍时本次读数 MUST 按 0 参与换算,原始读数照常记录,并标记当天触发过防旧值。比较基准为 0 时,不论配置的重置日是几号,重置日当天任何大于 0 的读数 MUST 按 0 参与换算。重置日次日起不再判断,读数照常写入;重置日当天没有取到有效读数的,次日起照常按新周期计。重置日当天采到的上周期读数 MUST NOT 记回前一天,上周期最后一次写入到运营商清零之间的用量不补。
|
||||
|
||||
#### Scenario: 重置日读到上周期旧值
|
||||
- **WHEN** 重置日为 1 的卡在 1 号读到超过上月最大快照 0.9 倍的读数
|
||||
- **THEN** 当天本月累计按 0 记、标记触发防旧值;次日读到的读数照常写入
|
||||
|
||||
#### Scenario: 重置日当天真实用量较大
|
||||
- **WHEN** 重置日当天真实用量超过比较基准的 0.9 倍
|
||||
- **THEN** 当天按 0 记,次日如实写入,这部分用量随次日快照计入,不丢失
|
||||
|
||||
#### Scenario: 重置日为 27 且比较基准为 0
|
||||
- **WHEN** 重置日为 27 的卡本月 1~26 号没有快照,27 号读到 3010 MB
|
||||
- **THEN** 当天按 0 记、标记触发防旧值;28 号读数照常写入
|
||||
|
||||
#### Scenario: 重置日早上的旧数
|
||||
- **WHEN** 重置日 00:10 采到上周期读数 3010 MB 并被写入
|
||||
- **THEN** 系统按 0 计入当天,不更新前一天的快照
|
||||
|
||||
### Requirement: 已切换卡按月对账产生增量
|
||||
系统 SHALL 对全部卡在写入点写入当天快照后计算增量 `Δ = 本月最大快照 − 本月各天已结算合计`,卡带待建起点标记时按"待建起点"处理。`Δ > 0` 且采集时刻卡有套餐(按"有套餐判断口径")时,系统写一条流量增量事件(携带采集时间与采集 ID)并把 `Δ` 计入当天已结算;`Δ > 0` 但没有套餐时 MUST NOT 写事件,只把 `Δ` 计入当天已结算,之后购买套餐 MUST NOT 补扣;`Δ ≤ 0` 时不写事件。采集时刻有套餐的卡当天第一次写入时,系统 SHALL 在同一事务内为采集时刻每个有效套餐确保当天一行套餐日用量(没有则写 0)。快照、已结算合计、套餐日用量 0 行与事件 MUST 同事务提交或回滚。每个写入点最多写一条事件,系统 MUST NOT 再按卡表基线计算增量。跨月时新月份已结算合计从 0 开始。
|
||||
|
||||
#### Scenario: 二次销售不带入上一个用户的用量
|
||||
- **WHEN** 卡在没套餐期间被写入本月累计 3020 MB,激活新套餐后写入 3050 MB
|
||||
- **THEN** 3020 MB 及之前的增量只计入已结算不写事件,新套餐只被扣 30 MB
|
||||
|
||||
#### Scenario: 迟到观测抬高昨天的快照
|
||||
- **WHEN** 今天已发出增量后,一次迟到写入把昨天的快照抬高
|
||||
- **THEN** 增量仍按本月最大快照减本月已结算合计计算,重叠部分不被扣两次
|
||||
|
||||
#### Scenario: 有套餐但当天没用流量
|
||||
- **WHEN** 有套餐的卡当天第一次写入,增量为 0
|
||||
- **THEN** 系统不写事件,为每个有效套餐写当天日用量 0
|
||||
|
||||
#### Scenario: 只有待生效套餐
|
||||
- **WHEN** 卡与绑定设备只有待生效套餐
|
||||
- **THEN** 增量只计入已结算,不写事件
|
||||
|
||||
### Requirement: 已切换卡的通道阈值判定
|
||||
系统 SHALL 只在写入点做通道阈值判定:本次写入点读数大于 0 且未被重置日防旧值按 0 处理时,用该读数做通道阈值判定。被队列拦下的读数、无效读数 MUST NOT 参与判定。
|
||||
|
||||
#### Scenario: 达量
|
||||
- **WHEN** 写入点读数达到通道阈值
|
||||
- **THEN** 系统写通道阈值停机锁并创建停机任务
|
||||
|
||||
#### Scenario: 被拦下的暴涨读数
|
||||
- **WHEN** 队列中间点读数超过通道阈值但被队列拦下
|
||||
- **THEN** 系统不做通道阈值判定
|
||||
|
||||
#### Scenario: 重置日读到旧总量
|
||||
- **WHEN** 卡在重置日写入上周期旧总量并触发防旧值
|
||||
- **THEN** 本次不做通道阈值判定;同一天之后的真实读数照常判定
|
||||
|
||||
### Requirement: 已切换卡的本月用量与日流量展示
|
||||
系统 SHALL 对全部卡在写入点写入后用本月最大快照覆盖卡表本月用量,使资产详情、卡列表与设备导出显示快照口径的本月用量;日流量查询 MUST 读取快照,某天用量为当天最大快照减本月此前最大快照,小于 0 按 0,本月第一条快照减数为 0。卡表 `last_gateway_reading_mb` 不再使用。
|
||||
|
||||
#### Scenario: 展示本月用量
|
||||
- **WHEN** 卡写入点写入本月累计 820 MB
|
||||
- **THEN** 资产详情、卡列表与设备导出的本月用量为 820 MB
|
||||
|
||||
#### Scenario: 中间几天没写入
|
||||
- **WHEN** 卡连续 3 天没有快照
|
||||
- **THEN** 这 3 天的用量并入之后第一个有快照的那天,不丢失
|
||||
|
||||
### Requirement: 流量按观测时间归属
|
||||
系统 SHALL 为每次流量采集记录网关返回时的采集时间,并以写入点的采集时间按"有套餐判断口径"确定本次增量是否扣减及扣减哪些套餐;采集时间早于套餐激活时间的,该套餐不算有套餐,增量 MUST 只记为已结算、不扣减该套餐。该规则 MUST 对轮询、激活前结算、人工刷新全部生效。
|
||||
|
||||
#### Scenario: 慢返回的结算晚于另一入口激活
|
||||
- **GIVEN** 资产无生效套餐,激活前结算 10:00:00 发出查询,另一入口 10:00:02 激活了套餐
|
||||
- **WHEN** 结算结果 10:00:05 返回并入账,采集时间早于激活时间
|
||||
- **THEN** 本次增量只记已结算,不扣新套餐
|
||||
|
||||
#### Scenario: 激活后的观测正常扣减
|
||||
- **WHEN** 写入点采集时间晚于套餐激活时间
|
||||
- **THEN** 增量按扣减规则扣减套餐
|
||||
|
||||
### Requirement: 激活前流量结算
|
||||
系统 SHALL 在资产从"无生效主套餐"变为"有生效主套餐"之前(实名激活、排队激活、指定激活、队首激活、订单购买即生效),对独立卡查一次流量、对设备逐张查设备下各卡,读数照常经过采集队列,然后再执行激活。本次读数满足写入条件时按观测入账(此时无套餐,增量只记已结算);不满足写入条件时,系统 MUST 用卡上已有的本月最大快照做结算(把它与本月已结算合计的差额全部记为已结算),不新写快照,采集记录判断记为"激活前结算用已有快照"。已有生效主套餐(续费、排队衔接)与流量重置 MUST NOT 执行结算。结算观测 MUST 携带"激活前结算"标记,入账照常但 MUST NOT 触发停复机评估;激活提交后的评估是激活链路唯一一次评估。结算查询 MUST 受运营商账户限流约束;单卡限流等待超过单步超时、查询失败或整次结算达到总超时时,系统 MUST 跳过剩余结算、照常激活,并按卡写告警。
|
||||
|
||||
#### Scenario: 二次销售激活
|
||||
- **GIVEN** 卡上一用户本月已用 3 GB,当前无生效套餐
|
||||
- **WHEN** 新用户购买套餐即生效
|
||||
- **THEN** 系统先查流量并把 3 GB 记为已结算,再激活,新套餐不承担这 3 GB
|
||||
|
||||
#### Scenario: 结算读数未满足写入条件
|
||||
- **WHEN** 激活前结算的读数使队列未满或出现下降
|
||||
- **THEN** 系统用已有本月最大快照结算,不新写快照,再激活
|
||||
|
||||
#### Scenario: 结算不触发停机
|
||||
- **WHEN** 结算观测入账时资产无套餐
|
||||
- **THEN** 系统不生成"无有效套餐"停机,激活提交后再评估一次停复机
|
||||
|
||||
#### Scenario: 结算超时
|
||||
- **WHEN** 设备下第 3 张卡结算时整次结算达到总超时
|
||||
- **THEN** 系统跳过剩余卡、照常激活,并为未结算的卡写告警
|
||||
|
||||
## REMOVED Requirements
|
||||
|
||||
### Requirement: 切换水位
|
||||
**Reason**: 测试环境一次性全量切换,不再有"第一次切换观测";切换不重复扣改由迁移时全部卡打"待建起点"标记实现。
|
||||
**Migration**: 迁移时全部卡置待建起点标记;`tb_card_flow_switch` 不再读写,数据保留不删。
|
||||
|
||||
### Requirement: 按扣减载体灰度切换
|
||||
**Reason**: 测试环境一次性全量切换,全部卡走日快照计量,灰度名单与计量模式下线。
|
||||
**Migration**: 删除 `JUNHONG_TRAFFIC_CALC_MODE`、`JUNHONG_TRAFFIC_CALC_CARDS` 配置;卡表基线算法不再产生扣费增量。
|
||||
@@ -0,0 +1,64 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 按采集时刻确定扣减套餐
|
||||
系统 SHALL 在消费流量增量事件时,按事件携带的采集时间用 `card-traffic-metering` 的"有套餐判断口径"查找扣减套餐,不再只认扣费时刻状态为生效中的套餐。采集时刻有效、扣费时已用完或已到期的套餐 MUST 照扣。扣减顺序、每个套餐的容量(启用虚流量时为虚总量,否则为真总量)、最后一个套餐承接超出部分、小数部分累积的规则保持不变。采集时间早于套餐最近一次流量重置、扣费在重置之后的,增量 MUST 扣在重置后的周期,审计记录采集时间与该套餐最近重置时间。
|
||||
|
||||
#### Scenario: 采集后套餐到期
|
||||
- **WHEN** 采集时间为套餐到期前 1 分钟,扣费时套餐已到期
|
||||
- **THEN** 系统照常扣减该套餐
|
||||
|
||||
#### Scenario: 用完到停机之间的用量
|
||||
- **WHEN** 卡唯一套餐已用完但仍在有效期,写入点产生增量
|
||||
- **THEN** 系统把增量计入该套餐的超出部分
|
||||
|
||||
#### Scenario: 跨套餐流量重置
|
||||
- **WHEN** 采集时间在日重置套餐 00:00 重置之前,扣费在重置之后
|
||||
- **THEN** 系统把增量扣在重置后的周期,审计中记录采集时间与重置时间
|
||||
|
||||
### Requirement: 扣减时的套餐状态迁移
|
||||
系统 SHALL 只对当前状态为生效中的套餐做"扣到阈值改为已用完"的状态迁移。当前状态为已用完、已过期的套餐 MUST 照常累加已用量与套餐日用量,MUST NOT 改变状态。余量小于等于 0 且不是最后一个套餐的跳过。停机判断 MUST 仍按套餐当前状态判断是否还有可用套餐。
|
||||
|
||||
#### Scenario: 已过期套餐被扣
|
||||
- **WHEN** 采集时刻有效的套餐在扣费时已过期,本次扣到超过阈值
|
||||
- **THEN** 系统累加已用量,套餐状态仍为已过期
|
||||
|
||||
#### Scenario: 生效中套餐扣到阈值
|
||||
- **WHEN** 生效中套餐本次扣减后达到阈值
|
||||
- **THEN** 系统把套餐改为已用完,并按当前状态判断是否停机
|
||||
|
||||
### Requirement: 扣费时查不到套餐
|
||||
采集时刻有套餐、扣费时一个套餐都查不到(采集后被退款或作废)时,系统 SHALL 记录告警日志与扣费审计(标明跳过原因),把事件按成功处理,MUST NOT 重试。该笔用量需要时由人工冲正处理。
|
||||
|
||||
#### Scenario: 采集后退款
|
||||
- **WHEN** 流量事件消费前,采集时刻唯一有效的套餐已退款
|
||||
- **THEN** 系统写跳过审计,事件按成功结束,不重试
|
||||
|
||||
### Requirement: 套餐日用量记录
|
||||
系统 SHALL 按 `(套餐使用记录, 日期)` 唯一保存套餐日用量,扣减时原子累加当天用量、覆盖累计用量,并发扣减 MUST NOT 产生重复行或丢失累加。日期 MUST 取写入点采集时间的上海日期,不取扣费时间;消费者的日流量缓冲同样按采集时间的上海日期归属。
|
||||
|
||||
#### Scenario: 凌晨采集隔一会儿才扣费
|
||||
- **WHEN** 写入点采集时间为上海时间 10-29 00:30,扣费在 00:31
|
||||
- **THEN** 套餐日用量记在 10-29
|
||||
|
||||
#### Scenario: 并发扣减同一套餐同一天
|
||||
- **WHEN** 两条流量事件同时扣减同一套餐同一天
|
||||
- **THEN** 当天只有一行日用量,当天用量为两次之和
|
||||
|
||||
### Requirement: 人工冲正
|
||||
系统 SHALL 提供人工冲正:调整套餐已用量只能经冲正流水进行,流水记录套餐使用记录、卡或设备、调整量(正数补扣、负数退回)、调整前后已用量、必填原因、可选关联采集记录与流量事件、操作人与时间。冲正 MUST 在事务内先锁定套餐行,按锁定后的当前已用量计算新值;调整后已用量小于 0 时 MUST 拒绝,不截成 0。同一事务内写流水、更新已用量并按阈值重算状态(已用完与生效中之间);已过期、已失效的套餐只改已用量不改状态。冲正 MUST 写审计。提交后系统 SHALL 调用停复机判断,MUST NOT 直接复机或停机。冲正 MUST NOT 修改卡日快照与已结算。
|
||||
|
||||
#### Scenario: 退回多扣的流量
|
||||
- **WHEN** 管理员对已用完的套餐冲正 −500 MB,冲正后低于阈值
|
||||
- **THEN** 套餐改为生效中,写流水与审计,提交后由停复机判断决定是否复机
|
||||
|
||||
#### Scenario: 退回后为负
|
||||
- **WHEN** 套餐已用 100 MB,管理员冲正 −200 MB
|
||||
- **THEN** 系统拒绝冲正,已用量不变
|
||||
|
||||
#### Scenario: 与扣减并发
|
||||
- **WHEN** 冲正与一笔流量扣减同时作用于同一套餐
|
||||
- **THEN** 两次修改都生效,不互相覆盖
|
||||
|
||||
#### Scenario: 卡因其他原因停机
|
||||
- **WHEN** 冲正后套餐不再用完,但卡因未实名停机
|
||||
- **THEN** 系统不复机
|
||||
@@ -0,0 +1,74 @@
|
||||
## 0. 前置
|
||||
|
||||
- [ ] 0.1 经维护者确认,归档 `rebuild-card-traffic-metering-and-stop-resume`(9.2–9.5 灰度与切换水位验证作废)与 `add-tiered-polling-and-pre-activation-settlement`,确认 `openspec/specs/card-traffic-metering/spec.md` 存在且包含本 Change 修改与删除的要求。
|
||||
|
||||
## 1. 迁移与模型
|
||||
|
||||
- [x] 1.1 新增成对迁移:采集记录表(按 `observed_at` 按月分区,字段见方案 7.1,索引 `(iot_card_id, observed_at DESC)`、`observation_id`),预建当月与下月分区;down 删除。
|
||||
- [x] 1.2 新增成对迁移:冲正流水表(字段见方案第 11 节);down 删除。
|
||||
- [x] 1.3 新增成对迁移:`tb_iot_card` 增加待建起点标记,up 内把全部卡置位;down 删除该列。
|
||||
- [x] 1.4 新增成对迁移:套餐日用量 `(package_usage_id, date)` 唯一索引,建前合并已有重复行;down 删除索引。(测试库已有唯一索引 `idx_package_usage_daily_record_unique` 且无重复行,无需新迁移)
|
||||
- [x] 1.5 新增对应 model;采集记录分区的按月新建与过期整月删除任务(保留 90 天)。
|
||||
|
||||
## 2. 读数有效性
|
||||
|
||||
- [x] 2.1 `internal/gateway/models.go`:流量查询结果的已用流量改为零值表示"没值"的类型,区分 null/空/无法解析/缺字段与 0;其他使用 `FlexFloat` 的字段不变。
|
||||
- [x] 2.2 `internal/task/polling_carddata_handler.go`:按方案第 3 节把结果分为有效、失败、限流、响应无效、无读数、读数无效。
|
||||
- [x] 2.3 `internal/domain/cardflow/rules.go`:`ValidReading` 放宽为有限且 ≥ 0;`isStale` 去掉 `ThisMonthBeforeResetMax > 0` 条件;同步注释。
|
||||
|
||||
## 3. 采集队列
|
||||
|
||||
- [x] 3.1 新增队列组件:key `traffic:queue:{card_id}`,Lua 原子完成跨天清空、过期丢弃、按采集时间插入去重、保留最后 N 个、设置过期(跨天清空开启时为次日 01:00 上海时间,关闭时为最长点龄后),返回队列与新点是否在队尾。
|
||||
- [x] 3.2 写入判断:新点在队尾、队列满 N、相邻两点不下降(容差内算持平);返回写入或不写原因(队列未满、读数下降、乱序)。
|
||||
- [x] 3.3 系统配置注册 `traffic.queue.size`(默认 3,最小 2)、`traffic.queue.clear_on_new_day`(默认 true)、`traffic.queue.max_point_age_hours`(默认 72)、`traffic.queue.drop_tolerance_mb`(默认 1),读法与 `package.silence.enabled` 相同(带短缓存)。
|
||||
- [x] 3.4 轮询配置保存与启用时、队列长度或跨天清空开关修改时,校验启用中轮询配置的流量采集间隔上限;停用配置不校验。
|
||||
|
||||
## 4. 有套餐判断与观测写入
|
||||
|
||||
- [x] 4.1 package 服务新增"采集时刻有效套餐"查询(状态生效中/已用完/已过期、`activated_at ≤ observed_at` 或为空、`expires_at` 为空或晚于采集时间、未退款,卡没有时看当前绑定设备),供观测与扣减共用。
|
||||
- [x] 4.2 `traffic_calc_policy.go`:`hasActivePackageForCard` 改调 4.1;删除灰度名单与计量模式;删除 `pkg/config` 中 `TrafficCalc` 的模式与名单配置及部署变量。
|
||||
- [x] 4.3 `traffic.go`、`traffic_flow.go`:只在队列写入点进入快照;删除未切换分支、`firstSwitch` 与切换水位读写;卡表基线算法不再产生扣费增量,`last_gateway_reading_mb` 不再使用。
|
||||
- [x] 4.4 写入范围:有套餐读数 0 写快照、没套餐读数 0 不写;Δ 有套餐发事件(带 `observed_at`、`observation_id`),没套餐只记已结算。
|
||||
- [x] 4.5 待建起点:带标记的卡第一次写入时写快照、本月最大全部记为已结算、不发事件、清标记,采集记录判断为建起点。
|
||||
- [x] 4.6 观测事务内当天第一次写入时,调用 package 服务为采集时刻每个有效套餐确保当天日用量 0 行。
|
||||
- [x] 4.7 通道阈值判定只在写入点、读数 > 0 且未触发防旧值时调用;卡表本月用量对全部卡用本月最大快照覆盖;日流量对全部卡读快照。
|
||||
- [x] 4.8 采集记录:有套餐的卡每次采集写一条(含失败、读数 0、被拦下),字段按方案 7.1。
|
||||
|
||||
## 5. 其他读数来源
|
||||
|
||||
- [x] 5.1 人工刷新过队列,接口返回本次读数、是否写入与原因;更新路由文档说明。
|
||||
- [x] 5.2 激活前结算过队列;未写入时用已有本月最大快照结算、不新写快照,采集记录判断为"激活前结算用已有快照";删除"未切换卡不做结算"的分支。
|
||||
|
||||
## 6. 换卡
|
||||
|
||||
- [x] 6.1 `internal/service/exchange`:换卡完成事务内给新卡置待建起点标记;设备换货时给新设备当前绑定的每张卡置位;不调用运营商接口。
|
||||
|
||||
## 7. 扣减
|
||||
|
||||
- [x] 7.1 `internal/infrastructure/cardobservation/event.go`:把 `observed_at`、`observation_id` 传给扣减;日流量缓冲 key 改用 `cardflow.ShanghaiDate(observed_at)`。
|
||||
- [x] 7.2 `DeductDataUsage` 按采集时刻调用 4.1 查套餐;排序、容量、超出承接与小数累积不变。
|
||||
- [x] 7.3 状态迁移只作用于当前生效中套餐;已用完、已过期只累加不改状态;停机判断仍按当前状态。
|
||||
- [x] 7.4 查不到套餐时写告警与跳过审计,事件按成功处理不重试。(没有套餐可挂审计资源,跳过只写带 metric 的告警日志)
|
||||
- [x] 7.5 采集早于套餐 `last_reset_at` 的照扣在重置后周期,审计记录 `observed_at` 与 `last_reset_at`。
|
||||
- [x] 7.6 `updateDailyRecord` 改为 upsert(当天用量累加、累计用量覆盖),日期取采集时间上海日期;提供"确保当天日用量行"方法。
|
||||
|
||||
## 8. 冲正与后台接口
|
||||
|
||||
- [x] 8.1 冲正用例:事务内 `SELECT ... FOR UPDATE` 锁套餐行,按当前值加减,结果 < 0 拒绝;写流水、更新已用量、按阈值重算状态(已过期/已失效只改已用量);写审计 `package_usage.adjust_traffic`;提交后调用 `evaluator.EvaluateAndAct`。
|
||||
- [x] 8.2 后台接口:冲正(必填原因、可选关联采集与事件)、冲正流水查询、采集记录按卡与时间范围查询;权限与现有套餐管理一致。(原"修改资产套餐已用量"接口改为按差额写冲正流水)
|
||||
- [x] 8.3 同步 `cmd/api/docs.go`、`cmd/gendocs/main.go`,运行 `go run cmd/gendocs/main.go`。
|
||||
|
||||
## 9. 告警
|
||||
|
||||
- [x] 9.1 开启轮询且有套餐的卡一整天没有已写入;开启轮询的卡待建起点超过 1 天;单卡读数下降占比过高或全局拦截率突增;乱序突增;重置日防旧值命中数统计。不轮询的卡不告警。
|
||||
|
||||
## 10. 构建与测试环境验证
|
||||
|
||||
- [x] 10.1 对变更 Go 文件执行 `gofmt -w`,运行 `go build ./cmd/api ./cmd/worker`、`openspec validate add-card-traffic-queue-and-observed-deduction --strict`。
|
||||
- [ ] 10.2 按 ENG-TEST-001 在测试环境执行迁移 up/down/up,核对新表、分区、唯一索引与全部卡待建起点标记。
|
||||
- [ ] 10.3 用构造的测试卡(参照 `cmd/zz_tmp_cardflow_verify`)逐项核对:正常累加、中间点暴涨被拦、清零前后不混写、容差内回退写入、乱序不写、跨天清空、读数 0 有/没套餐的写入范围、null 与缺字段不入队。
|
||||
- [ ] 10.4 重置日:配置重置日 1 与 27 各一张卡,核对重置日全天防旧值、比较基准为 0 时当天按 0 计且次日如实写入、重置日早上的旧数不更新前一天快照。
|
||||
- [ ] 10.5 全量切换与换卡:切换后第一次写入只建起点不扣费;换卡新卡写入 55 建起点、58 扣 3;起点读数异常不被当起点。
|
||||
- [ ] 10.6 扣减:已用完在有效期内的套餐承接超出;采集后到期照扣、状态不被改回已用完;采集后退款跳过不重试;套餐日用量按采集日期 upsert、没扣写 0。
|
||||
- [ ] 10.7 冲正:退回后状态恢复并交给停复机判断;结果为负被拒;与扣减并发不覆盖;因未实名停机的卡冲正后不复机。
|
||||
- [ ] 10.8 人工刷新返回写入结果;激活前结算未写入时用已有快照结算;采集记录可从扣费审计经采集 ID 追到;轮询配置间隔校验只拦启用中配置。
|
||||
Reference in New Issue
Block a user