修复金额的问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m30s

This commit is contained in:
2026-04-23 10:09:23 +08:00
parent 10a35625a5
commit 6b1fbf4a92
17 changed files with 637 additions and 16 deletions

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-04-22

View File

@@ -0,0 +1,36 @@
# 修复前基线证据(任务 1.1 / 1.2
## 采样时间
- 2026-04-22 18:00Asia/Shanghai
## 样本说明
- 采样条件:
- 卡已绑定设备(`tb_device_sim_binding.bind_status=1`
- 该卡无生效卡级套餐(`tb_package_usage.iot_card_id``status=1`
- 所属设备存在生效设备级套餐(`tb_package_usage.device_id``status=1`
## 关键基线数据MCP 查询)
### 1) 绑定设备卡流量已增长
| card_id | iccid | device_id | last_gateway_reading_mb | current_month_usage_mb | card_usage_mb | last_data_check_at |
|---|---|---:|---:|---:|---:|---|
| 4 | 89860885192590572650 | 2 | 2875.72 | 2875.72 | 2872 | 2026-04-22T09:44:22.355Z |
| 6 | 8986062575000172349 | 2 | 9047.27 | 9047.27 | 9047 | 2026-04-22T09:48:28.203Z |
| 7 | 89860624660021733454 | 3 | 33137.81 | 33137.81 | 33136 | 2026-04-22T09:48:50.758Z |
### 2) 对应设备级套餐未扣减
| device_id | package_usage_id | usage_type | status | data_usage_mb | package_updated_at |
|---:|---:|---|---:|---:|---|
| 2 | 5 | device | 1 | 0 | 2026-04-22T07:19:38.584Z |
| 3 | 6 | device | 1 | 0 | 2026-04-22T08:24:56.831Z |
## 基线结论
- 卡侧流量字段(`last_gateway_reading_mb` / `current_month_usage_mb` / `tb_iot_card.data_usage_mb`)已明显增长。
- 设备级生效套餐(`tb_package_usage.status=1``usage_type=device`)的 `data_usage_mb` 仍为 0。
- 可确认当前存在“卡流量增长但套餐未扣减”的问题基线,符合本提案修复目标。

View File

@@ -0,0 +1,80 @@
## Context
当前 `polling_carddata` 链路已经能正确更新卡维度流量读数,但套餐扣减存在两个缺口:
1. 载体路由缺口:扣减调用固定使用 `carrier_type=iot_card`,导致绑定设备且套餐挂在 `device_id` 时无法命中可扣套餐。
2. 精度缺口:上游流量读数是 `float64 MB`,扣减时转 `int64`,会丢失 `<1MB` 的高频增量。
系统现有分层为 Handler → Service → Store → Model扣减核心在 `UsageService.DeductDataUsage`,轮询和手动刷新都依赖该能力。为避免分叉逻辑,本次设计将修复集中在 Service 层并保持调用链兼容。
## Goals / Non-Goals
**Goals:**
- 修复绑定设备卡的套餐扣减命中问题,保证设备级套餐能被正确扣减。
- 修复小数增量丢失问题,保证高频小流量场景下长期扣减准确。
- 明确流量字段语义:`current_month_usage_mb` 与运营商周期口径解耦。
- 保持现有 API、数据库表结构和任务调度不破坏。
**Non-Goals:**
- 不重构轮询体系,不调整 Asynq 任务类型。
- 不新增数据库迁移。
- 不改变套餐优先级策略(仍是加油包优先、主套餐兜底)。
- 不新增自动化测试范围(按项目约束,仅提供手动验证方案)。
## Decisions
### 决策 1载体命中策略下沉到 UsageService
- 方案:`DeductDataUsage` 先按请求载体查询生效套餐;若 `iot_card` 未命中且该卡存在有效设备绑定,则回退按 `device` 查询并扣减。
- 理由:
- 轮询与手动刷新共用一套扣减逻辑,避免在多个 Handler/Service 重复实现路由判断。
- 符合分层约束,业务判断集中在 Service 层Store 层保持通用查询职责。
- 备选方案:在 `polling_carddata_handler` 单点改传 `device`
- 未采用原因:手动刷新链路仍会错;后续新入口也会重复踩坑。
### 决策 2小数增量使用“余量累加”而非直接四舍五入
- 方案:保留扣减单位为整数 MB但把小数部分按载体维度写 Redis 余量键(带 TTL后续增量先与余量相加再计算本次可扣整数 MB。
- 理由:
- 不改变 `tb_package_usage.data_usage_mb` 的整数结构,兼容现有统计与查询。
- 避免每次四舍五入带来的长期偏差。
- Redis 读写轻量,满足轮询高频场景性能要求。
- 备选方案:直接把 `data_usage_mb` 改为小数。
- 未采用原因:涉及模型、查询、统计口径和历史数据兼容,变更面过大。
### 决策 3字段口径保持双轨不再混用
- 方案:
- `current_month_usage_mb`:继续定义为“系统自然月累计(卡维度)”。
- `last_gateway_reading_mb`:用于表达“运营商当前周期累计读数(卡维度)”。
- 理由:
- 运营商重置日可能不是每月 1 号,不能用自然月字段替代运营商周期口径。
- 绑定设备不改变流量采集主体,流量仍来自卡 ICCID。
- 备选方案:让 `current_month_usage_mb` 直接改为运营商周期口径。
- 未采用原因:会破坏现有自然月统计语义,影响已有展示与分析逻辑。
## Risks / Trade-offs
- 风险:余量键读写失败时会影响小数累计精度。
- Mitigation失败仅降级为“本次不累计小数”并记录告警日志主流程不中断。
- 风险:`iot_card -> device` 回退可能在极端并发下出现重复判断。
- Mitigation扣减仍在事务内按当前生效套餐计算保持幂等结果。
- 风险:前端继续误用 `current_month_usage_mb` 作为运营商周期口径。
- Mitigation在 spec 和接口描述中显式标注口径,前端改读 `last_gateway_reading_mb`
## Migration Plan
1. 先上线 Service 层扣减修复(载体回退 + 小数余量)。
2. 通过日志与数据库手动核对重点卡(绑定设备卡、低流量高频卡)。
3. 前端切换运营商周期展示口径到 `last_gateway_reading_mb`
4. 观察 1-2 个运营商结算周期,无异常后关闭问题。
回滚策略:
- 若出现异常,可回滚到旧版本代码;该变更无 DB schema 迁移,不涉及数据结构回滚。
## Open Questions
- 余量 Redis 键 TTL 的最终值(建议 7 天)是否需要按运营商周期动态设置。
- 是否需要在管理端 realtime-status 直接返回 `last_gateway_reading_mb`(当前先由前端按现有可用字段接入)。

View File

@@ -0,0 +1,36 @@
## Why
当前线上存在“卡流量已同步但套餐流量不变化”的问题,导致计费与停复机判断失真。该问题集中出现在绑定设备的卡和小流量高频上报场景,已经影响流量扣减准确性,需要优先修复。
## What Changes
- feature-001-traffic-deduction-carrier-routing修正轮询流量扣减时的载体路由逻辑避免固定按 `iot_card` 扣减导致设备级套餐无法命中。
- feature-002-traffic-deduction-fractional-precision修正流量扣减精度消除 `float64 -> int64` 截断造成的 `<1MB` 增量长期丢失。
- feature-003-traffic-field-semantics-clarify明确字段语义与展示口径
- `current_month_usage_mb`:系统自然月累计流量(卡维度)
- `last_gateway_reading_mb`:运营商当前周期累计读数(卡维度)
- 保持现有分层架构与任务链路不变Handler → Service → Store → Model仅调整扣减判定与数据口径说明无 BREAKING API 变更。
## Capabilities
### New Capabilities
-
### Modified Capabilities
- `polling-task-handlers``polling_carddata_handler` 在触发套餐扣减时,需支持“卡绑定设备时按设备载体命中套餐”而非固定卡载体。
- `package-usage-priority``DeductDataUsage` 需支持小数 MB 增量累计后扣减,保证小流量高频上报场景下扣减准确。
- `asset-queries`:资产流量字段语义说明补充,避免将 `current_month_usage_mb` 误用为运营商周期口径。
## Impact
- 受影响代码:
- `internal/task/polling_carddata_handler.go`
- `internal/service/package/usage_service.go`
- `internal/service/iot_card/service.go`
- `internal/model/dto/asset_dto.go`(描述语义)
- 受影响数据:
- `tb_package_usage.data_usage_mb` 的增长将更贴近上游真实增量(尤其是 <1MB 高频增量场景)
- API 兼容性:
- 无接口路径和结构变更
- 前端展示口径建议改为读取 `last_gateway_reading_mb` 表达“运营商周期累计流量”
- 依赖与基础设施:无新增外部依赖、无数据库迁移。

View File

@@ -0,0 +1,14 @@
## ADDED Requirements
### Requirement: 卡流量字段口径必须明确且不可混用
系统对卡维度流量字段 SHALL 保持明确语义:
- `current_month_usage_mb`:系统自然月累计流量;
- `last_gateway_reading_mb`:运营商当前周期累计读数。
#### Scenario: 绑定设备不改变卡流量采集主体
- **WHEN** 卡已绑定设备并执行流量同步
- **THEN** 该卡流量字段仍按卡 ICCID 维度更新,不得被解释为设备聚合流量
#### Scenario: 运营商周期展示口径
- **WHEN** 前端需要展示“运营商周期累计流量”
- **THEN** 应使用 `last_gateway_reading_mb` 口径,不得使用 `current_month_usage_mb` 代替

View File

@@ -0,0 +1,16 @@
## ADDED Requirements
### Requirement: 流量扣减必须保留小数增量精度
系统处理上游流量增量时 SHALL 支持小数 MB 累计,不得因 `float64 -> int64` 截断导致长期漏扣。
#### Scenario: 多次小于 1MB 增量累计后触发扣减
- **WHEN** 同一载体连续收到 0.4MB、0.3MB、0.5MB 的增量
- **THEN** 系统累计小数余量后至少完成 1MB 套餐扣减,剩余小数继续保留
#### Scenario: 单次小数增量不足 1MB
- **WHEN** 本次增量为 0.2MB 且累计后仍小于 1MB
- **THEN** 系统不执行套餐 `data_usage_mb` 整数扣减,但必须保留该小数余量用于后续累计
#### Scenario: 轮询与手动刷新口径一致
- **WHEN** 流量增量分别来自轮询任务和手动刷新
- **THEN** 两条链路使用一致的小数累计扣减规则,不能出现一条链路扣减、另一条链路漏扣

View File

@@ -0,0 +1,18 @@
## ADDED Requirements
### Requirement: 流量轮询扣减必须支持设备载体回退
系统在 `polling_carddata_handler` 触发套餐扣减时 SHALL 支持载体自动命中:
- 优先按 `iot_card` 载体扣减;
- 当卡载体无生效套餐且该卡存在有效设备绑定时,回退按 `device` 载体扣减。
#### Scenario: 绑定设备卡命中设备级套餐
- **WHEN** 轮询检测到卡有正向流量增量,且该卡 `iot_card_id` 下无生效套餐、但其 `device_id` 下有生效套餐
- **THEN** 系统使用 `device` 载体执行扣减,套餐 `data_usage_mb` 正常增长
#### Scenario: 独立卡保持卡载体扣减
- **WHEN** 轮询检测到独立卡有正向流量增量
- **THEN** 系统继续按 `iot_card` 载体扣减,不引入设备载体分支
#### Scenario: 卡与设备都无生效套餐
- **WHEN** 轮询检测到流量增量,但卡载体与回退设备载体均无可用套餐
- **THEN** 系统记录“无可用套餐”并保持任务链路可继续重入队,不得中断轮询主流程

View File

@@ -0,0 +1,29 @@
## 1. 基线确认
- [x] 1.1 用目标卡/设备样本确认当前问题基线:卡流量增长但套餐 `data_usage_mb` 不增长
- [x] 1.2 记录基线证据(关键日志、`tb_iot_card``tb_package_usage` 当前值)用于修复后对比
## 2. 载体路由修复
- [x] 2.1 调整流量扣减链路,确保扣减入口支持“`iot_card` 未命中时回退 `device` 载体”
- [x] 2.2 在 `UsageService` 内统一实现载体命中逻辑,避免轮询与手动刷新分叉
- [x] 2.3 手动验证绑定设备卡场景:设备级套餐 `data_usage_mb` 可随流量同步增长
## 3. 小数流量精度修复
- [x] 3.1 调整扣减入参与内部计算口径,支持 `float64` 增量处理
- [x] 3.2 增加“流量小数余量”Redis 键与读写逻辑,累计后再执行整数 MB 扣减
- [x] 3.3 手动验证 `<1MB` 高频增量场景:多次同步后套餐扣减累计值正确
## 4. 字段语义对齐
- [x] 4.1 更新相关 DTO/文档描述,明确 `current_month_usage_mb` 是自然月口径
- [x] 4.2 在文档中明确 `last_gateway_reading_mb` 用于运营商周期累计口径展示
## 5. 回归与验收
- [x] 5.1 手动回归轮询与手动刷新两条链路,确认扣减逻辑一致
- [x] 5.2 用 PostgreSQL MCP 复核关键数据:`tb_iot_card.last_gateway_reading_mb``tb_package_usage.data_usage_mb` 变化符合预期
- [x] 5.3 执行 `openspec status --change fix-traffic-deduction-on-device-card`,确认提案进入可执行状态
> 说明2.3、3.3、5.1、5.2 由用户手动验证并确认通过。