Files
junhong_cmp_fiber/openspec/changes/refactor-traffic-model-semantics/design.md
huang fe4c545308
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m55s
修正数据
2026-04-27 18:10:08 +08:00

9.7 KiB
Raw Blame History

Context

当前仓库中的流量语义分散在三层:

  1. 同步层tb_iot_card.last_gateway_reading_mbcurrent_month_usage_mbdata_usage_mb 负责承接上游累计量与增量。
  2. 套餐层tb_package_usage.data_limit_mbdata_usage_mb 负责真实使用量累计、主套餐/加油包优先级扣减、状态流转与停复机联动。
  3. 展示层:后台 asset-resolve / current-package / packages、C 端 asset/info / package-history、客户视图流量接口,又额外暴露了 package_total_mbpackage_used_mbvirtual_limit_mbvirtual_ratio 等旧字段,且不同接口对 ratio 有“乘”和“除”两种口径。

本次变更不是新增一个流量接口,而是把“真实消耗事实”“停机阈值”“前端展示映射”三件事拆开并统一命名。已确认的业务共识如下:

  • virtual_total_mb 永远大于 0 且小于等于 real_total_mb
  • 一个载体同一时刻只有一个生效主套餐,可以存在多个生效/已用完的加油包
  • 对外必须统一返回:real_total_mbreal_used_mbvirtual_total_mbvirtual_used_mbreduction_pct
  • virtual_used_mb 不是简单的“缩小后的已用量”,而是把真已用映射回真总量刻度后的展示值
  • 旧的模糊字段最终要退出,避免继续误导后续开发和前端消费方

当前约束:

  • 保持 Handler → Service → Store → Model 分层,不引入新依赖
  • 数据库仍使用 GORM 与手动 ID 关联,不使用外键 / GORM 关联标签
  • 停复机、轮询、扣减、展示接口都要共用同一套语义,不能只改 DTO 名称

Goals / Non-Goals

Goals:

  • 统一套餐流量事实字段,明确 tb_package_usage 才是业务事实源
  • 为套餐使用记录补充虚总量与展示映射快照,避免历史展示继续读取 live tb_package
  • 统一 virtual_used_mbreduction_pct 的计算规则
  • 统一主套餐 / 加油包在后台、C 端、停复机中的流量语义
  • 用一次重构完成外部 DTO / OpenAPI / 文档的字段清理,不长期保留误导字段

Non-Goals:

  • 不重写卡同步增量算法本身(仍由网关累计量驱动)
  • 不重构主套餐排队机制与加油包生命周期拓扑(继续沿用 master_usage_id + priority
  • 不在本次变更中新增全新的停机原因体系或网关接入方式
  • 不在本次提案中引入自动化测试框架变更执行阶段继续以手工验收、PostgreSQL 核验和接口回放为主
  • 不在本次变更中设计或重构 customer-view 的 total 聚合语义

Decisions

决策 1统一事实层与展示层字段含义

决策

  • real_total_mb:套餐真实总量,内部仍以 tb_package_usage.data_limit_mb 作为真实总量快照
  • real_used_mb:套餐真实已用,内部仍以 tb_package_usage.data_usage_mb 作为真实已用累计值
  • virtual_total_mb:套餐业务停机阈值,需要落到 tb_package_usage 快照字段,不再直接依赖 tb_package.virtual_data_mb
  • virtual_used_mb:前端展示值,定义为 min(real_used_mb * display_gain_ratio, real_total_mb)
  • display_gain_ratio:内部展示映射比例,定义为 real_total_mb / virtual_total_mb
  • reduction_pct:对外字段名继续沿用“缩减比例”业务口语,但公式固定为 display_gain_ratio - 1,用于表达“真已用映射到虚已用时的增幅”;接口返回格式固定为 0.428571 这种比例小数,而不是 42.8571

为什么这样做

  • real_used_mbvirtual_used_mb 是两种不同语义:前者是事实,后者是展示
  • 继续沿用“旧 virtual_ratio 既当内部比例又当外部字段”的方式,仍会导致接口消费者误读
  • 用内部 display_gain_ratio + 外部 reduction_pct 的分离命名,可以同时保住公式正确性和对外可读性

备选方案

  • 继续直接对外暴露 virtual_ratio:实现简单,但会继续把“倍率”和“百分比”混为一谈
  • 只对外暴露真值 + 比例,让前端自己算虚已用:会把业务曲线责任下放到前端,导致多端不一致

决策 2停机判断统一按套餐使用快照阈值执行

决策

  • 启用虚流量时,套餐耗尽阈值为 virtual_total_mb_snapshot
  • 未启用虚流量时,套餐耗尽阈值为 real_total_mb
  • 套餐剩余额度统一定义为 effective_threshold_mb - real_used_mb
  • 套餐状态置为“已用完”的条件,统一改为 real_used_mb >= effective_threshold_mb
  • 载体停机条件统一为:没有任何 active 套餐仍有剩余业务额度

为什么这样做

  • 当前 data_limit_mb/data_usage_mb 的比较逻辑只适用于“真总量即停机阈值”的旧模型
  • 本次业务已经明确 virtual_total_mb 是业务停机阈值,因此必须在扣减、耗尽、停机、复机四条链路使用同一阈值定义

备选方案

  • 继续按 data_limit_mb 判耗尽,只在展示层补充虚流量字段:会产生“展示说用完了但系统不停机”或“系统停机了但展示还没满”的双重口径

决策 3历史展示不再依赖 live tb_package

决策:在 tb_package_usage 新增至少以下快照字段:

  • virtual_total_mb_snapshot
  • display_gain_ratio_snapshot
  • enable_virtual_data_snapshot

历史回填按当前 Package 数据做一次 best-effort 回填,并在设计与实施文档中明确这是“语义收口后的基线”,不承诺恢复所有历史上已经漂移过的展示事实。

为什么这样做

  • 当前 B/C 端多个历史流量展示接口仍会读取 live tb_package 值,套餐模板变更后历史详情会漂
  • 本次要清理旧字段,就必须先把历史可回放的最小快照集补齐

备选方案

  • 完全不回填,仅对新创建套餐使用记录生效:能降低迁移复杂度,但同一接口会同时出现新老两套语义

决策 4接口层按“主套餐视图”和“套餐记录视图”分开定义

决策

  • current-package 与资产摘要类接口只代表当前生效主套餐
  • packagespackage-history 代表按 package_usage_id 组织的套餐记录视图
  • 主套餐与加油包共存关系继续通过 master_usage_idpriority 表达,不引入新的关系模型
  • asset-resolveasset/info 返回当前主套餐时同步返回 current_package_usage_id
  • current-package 在无生效主套餐时返回 200 + null,而不是 404
  • 当前未正式暴露到路由层的 customer-view total 聚合不纳入本次 contract 收口范围

为什么这样做

  • 当前业务已经确认“一个 active 主套餐 + 多个加油包”是稳定模型
  • 如果资产摘要接口试图把主套餐和多个加油包重新聚成一个唯一流量总览,会再次把事实层和展示层混在一起

备选方案

  • 在所有资产接口里统一返回聚合总览:前端方便,但会牺牲 package_usage 作为业务事实源的清晰性

决策 5采用一次 breaking 重构,而不是长期兼容旧字段

决策

  • 本变更允许在一个版本窗口内完成数据库迁移、服务重构、DTO 替换、OpenAPI 更新和文档切换
  • 旧字段不作为长期兼容字段保留;除迁移执行所需的短暂过渡外,确认废弃的数据库旧列也在本次变更中一并物理删除

为什么这样做

  • 当前旧字段名称本身已经是误导源,长期并存只会让后续代码继续写出“半新半旧”的逻辑
  • 用户已经明确希望推倒重来,不希望遗留字段继续误导开发者

备选方案

  • 新旧字段并存多个版本:对前端更温和,但会明显提高 Service、DTO、OpenAPI、文档和人工验收成本

Risks / Trade-offs

  • [字段命名仍可能让人误解] → 在 spec 与 design 中同时定义“内部展示倍率”和外部 reduction_pct 的精确定义,并要求 DTO 命名避免继续使用旧 virtual_ratio
  • [历史回填无法百分百恢复真实历史展示] → 本次接受 best-effort 回填,并在验收中重点核对回填基线样例;如需完全回放,后续单独发起“历史流量审计基线”变更
  • [停机逻辑与展示逻辑同时改动,容易联调混乱] → 按“写链路先收口、读链路后切换、旧字段最后删除”的顺序执行
  • [资产摘要是否展示主套餐还是聚合视图存在前端预期差] → 在 spec 中显式写死 current-package 与资产摘要的语义,不允许实现层各自解释
  • [比例字段口径争议] → 将字段名称保留为 Open Question在 proposal/spec 已明确公式的前提下允许后续改名但不改语义

Migration Plan

  1. 新增 tb_package_usage 快照字段并执行历史回填。
  2. 重构套餐创建/激活路径,确保新写入的 PackageUsage 同时落真实总量、虚总量、展示倍率快照。
  3. 重构扣减、耗尽、停机、复机逻辑,统一改为按 effective_threshold_mb 判断。
  4. 重构后台 asset-resolve / current-package / packages 与 C 端 asset/info / package-history 的 DTO 和组装逻辑。
  5. 更新后台/C 端 OpenAPI、文档和手工验收脚本。
  6. 移除旧 DTO 字段、旧组装逻辑和确认废弃的数据库旧列,完成一次性语义收口。

Rollback

  • 数据库迁移阶段保留核心真实用量列 data_limit_mb / data_usage_mb,新增快照字段可逆
  • 在物理删除旧列前,可通过切回旧读链路快速回滚
  • 一旦完成旧列删除与 contract 切换后,不再承诺对旧客户端兼容

Open Questions

  • 当前无阻塞实施的开放问题;若后续需要继续优化,仅允许围绕字段文案与展示文案做非语义性微调,不再更改公式、返回结构或 current-package 的空结果契约。