# 流量模型统一改造方案 ## 1. 背景 当前项目中的流量逻辑同时存在以下几套口径: 1. **卡同步口径** - `tb_iot_card.last_gateway_reading_mb` - `tb_iot_card.current_month_usage_mb` - `tb_iot_card.data_usage_mb` 2. **套餐使用口径** - `tb_package_usage.data_limit_mb` - `tb_package_usage.data_usage_mb` 3. **展示换算口径** - `tb_package.virtual_ratio` - B 端/C 端多个接口中对 `virtual_*` 字段的换算 这三套口径目前没有统一到同一业务模型上,导致以下问题: 1. 流量同步与流量展示不是同一套业务事实源。 2. 停机判断仍按真总量而不是按虚阈值判断。 3. 历史套餐展示依赖 `tb_package` 当前值,缺少使用记录快照。 4. 资产视角接口和套餐视角接口混用,前端不知道该信哪一个字段。 5. 已存在字段 `real_data_usage_mb`、`virtual_data_usage_mb` 没有进入主链路,语义噪音较大。 本方案的目标不是“小修展示口径”,而是**统一整个项目的流量业务模型**。 --- ## 2. 核心结论 ### 2.1 业务事实源统一 **结论:业务流量永远跟随套餐,不跟随卡。** - **卡表(`tb_iot_card`)** 只负责: - 同步上游网关累计流量 - 计算增量 - 记录同步状态与同步时间 - 排查上游异常 - **套餐使用表(`tb_package_usage`)** 才是: - 停机判断事实源 - B 端资产流量展示事实源 - C 端流量展示事实源 - 套餐历史与当前套餐流量事实源 ### 2.2 流量摘要主体统一 **结论:流量摘要的主体应为 `package_usage_id`,不是 `asset_id`,也不是 `package_id`。** 原因: 1. 同一个资产可能存在多条套餐使用记录(主套餐、加油包、历史套餐)。 2. 同一个 `package_id` 会被多个资产、多次购买复用。 3. 前端真正要看的,是某一条具体套餐使用记录当前的四个业务值。 因此,流量摘要应以 **`tb_package_usage.id`** 为主键进行查询和展示。 ### 2.3 前端最终只看四个业务值 本次统一后的对外流量模型只保留四个业务值: 1. `real_total_mb`:真总量 2. `real_used_mb`:真已用 3. `virtual_total_mb`:虚总量(原始虚阈值) 4. `virtual_used_mb`:按比例换算后、与真总量同尺度的展示值 说明: - 用户已确认:`virtual_total_mb` 使用**原始虚总量**,不是换算后的总量。 - 用户已确认:`virtual_used_mb` 使用**按比例换算后的展示值**。 --- ## 3. 统一后的业务定义 ### 3.1 套餐模板层(`tb_package`) 套餐模板保留以下字段定义: - `real_data_mb`:套餐真总量 - `virtual_data_mb`:套餐虚总量 / 虚阈值 - `enable_virtual_data`:是否启用虚流量 - `virtual_ratio`:内部展示倍率,公式为 `real_data_mb / virtual_data_mb` ### 3.2 套餐使用层(`tb_package_usage`) 套餐使用记录作为最终业务事实源,统一定义为: - `data_limit_mb`:**真总量快照** - `data_usage_mb`:**真已用** - `virtual_total_mb_snapshot`:**虚总量快照** - `display_gain_ratio_snapshot`:**展示倍率快照** - `enable_virtual_data_snapshot`:**是否启用虚流量快照** ### 3.3 四个业务值公式 对于任意一条 `package_usage`,统一用以下公式计算: #### 场景 A:启用虚流量且 `virtual_total_mb_snapshot > 0` - `real_total_mb = data_limit_mb` - `real_used_mb = data_usage_mb` - `virtual_total_mb = virtual_total_mb_snapshot` - `virtual_used_mb = min(real_used_mb * display_gain_ratio_snapshot, real_total_mb)` - `reduction_pct = display_gain_ratio_snapshot - 1` #### 场景 B:未启用虚流量 - `real_total_mb = data_limit_mb` - `real_used_mb = data_usage_mb` - `virtual_total_mb = data_limit_mb` - `virtual_used_mb = real_used_mb` 说明: 1. 未启用虚流量时,虚流量视图退化为真流量视图,保证前端仍然能稳定收到四个值。 2. `virtual_used_mb` 的上限必须封顶到 `real_total_mb`,避免出现展示值超过真总量。 ### 3.4 停机判断公式 统一后的停机判断必须基于套餐使用记录: #### 场景 A:启用虚流量且 `virtual_total_mb_snapshot > 0` - 当 `real_used_mb >= virtual_total_mb` 时,套餐视为已耗尽 #### 场景 B:未启用虚流量 - 当 `real_used_mb >= real_total_mb` 时,套餐视为已耗尽 这一定义与本次 OpenSpec 的典型样例一致: - 真 `100` / 虚 `70`:真已用到 `70` 时停机,展示 `virtual_used_mb = 100` - 未启用虚流量:真已用到 `100` 时停机,展示 `virtual_used_mb = real_used_mb` ### 3.5 `virtual_data_mb` 校验规则 本次统一后的规则为: 1. `enable_virtual_data = true` 时,`virtual_data_mb` 必须大于 `0` 2. `enable_virtual_data = true` 时,`virtual_data_mb` 必须小于等于 `real_data_mb` 3. 未启用虚流量时,展示视图退化为真流量视图 --- ## 4. 同步链路统一方案 ### 4.1 卡同步链路保留,但降级为“同步事实链路” 当前同步链路的核心算法可以保留: 1. 从网关读取当前累计流量读数 `Used` 2. 使用 `Used - last_gateway_reading_mb` 计算本次增量 3. 遇到上游重置窗口时,将当前读数视为新周期增量 该链路应继续写入卡表: - `tb_iot_card.last_gateway_reading_mb` - `tb_iot_card.current_month_usage_mb` - `tb_iot_card.current_month_start_date` - `tb_iot_card.last_month_total_mb` - `tb_iot_card.last_sync_time` - `tb_iot_card.last_data_check_at` ### 4.2 卡同步增量如何进入套餐层 同步出的真流量增量必须继续扣减到 `tb_package_usage.data_usage_mb`。 统一口径: - **同步层永远只同步“真流量增量”** - **套餐层永远只累加“真已用”** - **虚流量不参与同步,只参与展示换算和停机阈值判断** ### 4.3 `tb_iot_card.data_usage_mb` 的处理 当前 `tb_iot_card.data_usage_mb` 表示卡生命周期累计流量,但它不是业务展示主来源。 本次建议: - **保留字段** - **降级为同步辅助/运维诊断字段** - 不再作为 B 端/C 端业务展示的主流量来源 说明: 1. 当前该字段写入时直接使用 `int64(increment)`,存在小数截断。 2. 套餐层已通过 `UsageService` 的余量缓存机制处理小数累计,更适合作为业务事实源。 --- ## 5. 展示链路统一方案 ### 5.1 资产视角不再直接承载流量摘要 `asset` 本身不是流量摘要的正确主体,因为一个资产可能有多条套餐使用记录。 因此: - `资产详情` - `资产解析` - `C 端资产信息` 不应再把流量摘要作为“资产级别唯一摘要”长期保留。 ### 5.2 套餐视角承载流量摘要 流量摘要应绑定到具体的 `package_usage_id`: - B 端:资产套餐列表 / 当前套餐 - C 端:当前套餐 / 套餐历史 / 套餐流量详情 都应围绕 `package_usage_id` 返回四个业务值。 ### 5.3 建议的接口模型 #### 方案 A:现有套餐接口直接返回四值 - `GET /api/admin/assets/:identifier/packages` - `GET /api/admin/assets/:identifier/current-package` - `GET /api/c/v1/asset/package-history` 每条 `AssetPackageResponse` 直接返回: - `real_total_mb` - `real_used_mb` - `virtual_total_mb` - `virtual_used_mb` #### 方案 B:新增“套餐流量摘要”接口 按 `package_usage_id` 查询单条套餐流量摘要,例如: - `GET /api/admin/asset-packages/:package_usage_id/traffic-summary` - `GET /api/c/v1/asset/package-usages/:package_usage_id/traffic-summary` 推荐做法: 1. 套餐列表接口仍返回四值,方便列表页直接展示 2. 同时提供单条摘要接口,供详情弹窗/详情页按 `package_usage_id` 精确查询 ### 5.4 本次建议下线的旧展示字段 以下字段语义模糊,应从对外 DTO 中逐步移除: - `package_total_mb` - `package_used_mb` - `package_remain_mb` - `virtual_remain_mb` 替代为显式字段: - `real_total_mb` - `real_used_mb` - `virtual_total_mb` - `virtual_used_mb` --- ## 6. 数据库改造方案 ### 6.1 本次必须新增的字段 在 `tb_package_usage` 新增: 1. `virtual_total_mb_snapshot BIGINT NOT NULL DEFAULT 0` 2. `display_gain_ratio_snapshot DECIMAL(18,6) NOT NULL DEFAULT 1.0` 3. `enable_virtual_data_snapshot BOOLEAN NOT NULL DEFAULT FALSE` ### 6.2 为什么必须加快照字段 如果不加快照,会有三个问题: 1. 历史套餐展示依赖 `tb_package` 当前值,模板一改,历史流量就漂 2. 历史停机判断无法复现当时口径 3. 退款、历史订单、历史套餐详情都无法稳定回放 ### 6.3 回填策略 对已有 `tb_package_usage` 数据执行回填: #### 若 `tb_package.enable_virtual_data = true` 且 `tb_package.virtual_data_mb > 0` - `virtual_total_mb_snapshot = tb_package.virtual_data_mb` - `display_gain_ratio_snapshot = tb_package.virtual_ratio` - `enable_virtual_data_snapshot = true` #### 否则 - `virtual_total_mb_snapshot = data_limit_mb` - `display_gain_ratio_snapshot = 1.0` - `enable_virtual_data_snapshot = false` ### 6.4 现有字段暂不重命名 本次不建议直接重命名以下数据库字段: - `tb_package_usage.data_limit_mb` - `tb_package_usage.data_usage_mb` 原因: 1. 改名会牵涉大量 SQL、模型、历史脚本、迁移与兼容问题 2. 当前可以通过“统一文义 + 新增快照字段”先完成业务收口 本次统一后的文义为: - `data_limit_mb` = 真总量 - `data_usage_mb` = 真已用 ### 6.5 最终可删除字段 以下字段当前未进入主链路,本轮已在迁移中物理删除: - `tb_package_usage.real_data_usage_mb` - `tb_package_usage.virtual_data_usage_mb` 执行顺序: 1. 代码层先完全停止依赖它们 2. 再通过独立迁移删除数据库旧列 --- ## 7. 代码改造清单 ### 7.1 数据模型与迁移 需要改动: - `migrations/`:新增 `tb_package_usage` 快照字段迁移与历史回填 - `internal/model/package.go` 改动内容: 1. `PackageUsage` 增加三类快照字段 2. 为 `data_limit_mb`、`data_usage_mb` 的注释补充“真总量 / 真已用”的语义说明 3. 为后续删除 `real_data_usage_mb`、`virtual_data_usage_mb` 做准备 ### 7.2 套餐创建与激活链路 需要改动: - `internal/service/order/service.go` - `internal/task/auto_purchase.go` 改动内容: 1. 创建 `PackageUsage` 时写入: - `data_limit_mb = pkg.RealDataMB` - `virtual_total_mb_snapshot` - `display_gain_ratio_snapshot` - `enable_virtual_data_snapshot` 2. 所有主套餐、加油包、自动购包路径保持一致 ### 7.3 套餐配置校验 需要改动: - `internal/service/package/service.go` 改动内容: 1. 保留“启用虚流量时必须填写虚流量”的校验 2. 收紧为 `virtual_data_mb > 0 且 <= real_data_mb` 3. `virtual_ratio` 继续使用 `real_data_mb / virtual_data_mb` ### 7.4 流量同步链路 需要改动: - `internal/task/polling_carddata_handler.go` - `internal/service/iot_card/service.go` 改动内容: 1. 核心同步算法保持不变 2. 明确注释与日志:该链路同步的是“真流量增量” 3. 保持将真增量扣减到套餐层 4. 不在该链路引入任何虚流量换算逻辑 ### 7.5 套餐扣减链路 需要改动: - `internal/service/package/usage_service.go` 改动内容: 1. `data_usage_mb` 继续作为真已用累加 2. 套餐是否“已用完”必须改为按“虚阈值快照或真总量”判断 3. 加油包 / 主套餐的耗尽逻辑全部统一到快照字段 4. 日记录 `PackageUsageDailyRecord` 继续记录真流量使用量 ### 7.6 停机判断链路 需要改动: - `internal/service/iot_card/stop_resume_service.go` 改动内容: 1. `isTrafficExhausted` 改为基于 `package_usage` 快照字段判断 2. 启用虚流量时,按 `real_used >= virtual_total_snapshot` 3. 未启用时,按 `real_used >= real_total` ### 7.7 套餐重置链路 需要改动: - `internal/service/package/reset_service.go` - `internal/store/postgres/package_usage_store.go` 改动内容: 1. 重置时继续清零 `data_usage_mb` 2. 重置后根据快照阈值恢复 `status=active` 3. 不引入额外的虚流量计数器 ### 7.8 后台管理接口 需要改动: - `internal/service/asset/service.go` - `internal/model/dto/asset_dto.go` - 如有前端依赖生成文档,还需同步更新 `docs/admin-openapi.yaml` 改动内容: 1. `AssetResolveResponse` 去掉资产级别模糊流量摘要字段 2. `AssetPackageResponse` 改为输出四个显式业务值: - `real_total_mb` - `real_used_mb` - `virtual_total_mb` - `virtual_used_mb` 3. `packages` / `current-package` 统一按 `package_usage_id` 展示和计算 ### 7.9 C 端接口 需要改动: - `internal/handler/app/client_asset.go` - `internal/model/dto/client_asset_dto.go` - `internal/service/package/customer_view_service.go` 改动内容: 1. C 端资产信息页不再把资产本身当作唯一流量摘要主体 2. C 端套餐历史/当前套餐改为套餐视角四值 3. 如果需要单条摘要页,则新增按 `package_usage_id` 查询的接口 ### 7.10 套餐商品展示联动确认 需要关注: - `internal/handler/app/client_asset.go` 中可购套餐列表当前 `data_allowance` 在启用虚流量时直接展示 `virtual_data_mb` 该处不是本次核心链路,但建议联动确认: 1. 商品展示要展示真流量、虚流量,还是只展示一种 2. 若前端也要透明展示两种总量,DTO 需补充显式字段 --- ## 8. DTO 与接口建议 ### 8.1 建议新增或替换的字段 建议在套餐相关 DTO 中统一使用: - `real_total_mb` - `real_used_mb` - `virtual_total_mb` - `virtual_used_mb` ### 8.2 建议废弃的字段 建议废弃: - `package_total_mb` - `package_used_mb` - `package_remain_mb` - `virtual_limit_mb` - `virtual_remain_mb` - `data_limit_mb`(对外 DTO 层) - `data_usage_mb`(对外 DTO 层) 说明: 数据库内部暂时仍可保留 `data_limit_mb` / `data_usage_mb`,但 DTO 层应改为语义明确的新字段。 ### 8.3 接口职责建议 #### 卡实时状态接口 保留同步诊断字段,例如: - `last_gateway_reading_mb` - `current_month_usage_mb` - `last_sync_time` #### 套餐流量接口 只返回四个业务值,不再掺杂卡级同步字段。 --- ## 9. 迁移与上线顺序 建议按以下顺序执行: ### 第一步:数据库准备 1. 新增 `tb_package_usage` 快照字段 2. 回填历史数据 3. 为后续删列保留可回滚窗口 ### 第二步:写链路切换 1. 套餐激活/自动购包路径写入快照字段 2. 扣减逻辑与停机逻辑改为读取快照字段 ### 第三步:读链路切换 1. B 端套餐接口改为返回四个业务值 2. C 端套餐接口改为返回四个业务值 3. 资产级模糊流量摘要降级或移除 ### 第四步:前端联调 1. 管理后台改为只消费套餐视角四值 2. C 端改为只消费套餐视角四值 3. 如需套餐流量详情页,按 `package_usage_id` 查询 ### 第五步:清理旧字段 1. 停止使用 `real_data_usage_mb` / `virtual_data_usage_mb` 2. 物理删除旧列 3. 对外移除旧 DTO 字段 --- ## 10. 手工验证清单 本项目不写自动化测试,本次改造建议用以下方式手工验证: ### 10.1 PostgreSQL 验证 验证项: 1. `tb_package_usage` 快照字段是否已正确回填 2. 启用虚流量的套餐是否满足: - `virtual_total_mb_snapshot = tb_package.virtual_data_mb` - `display_gain_ratio_snapshot = tb_package.virtual_ratio` 3. 未启用虚流量的套餐是否满足: - `virtual_total_mb_snapshot = data_limit_mb` - `display_gain_ratio_snapshot = 1.0` ### 10.2 上游同步验证 验证项: 1. 轮询后 `tb_iot_card.last_gateway_reading_mb` 是否更新 2. 同步增量是否正确进入 `tb_package_usage.data_usage_mb` 3. 跨月与运营商重置窗口时增量是否正确 ### 10.3 停机验证 验证项: 1. 真 `100` / 虚 `70`:真已用到 `70` 时停机 2. 真 `100` / 虚 `70` / 真已用 `69`:展示 `virtual_used_mb = 98.57` 3. 未启用虚流量:真已用到真总量时停机 ### 10.4 展示验证 验证项: 1. B 端套餐列表与当前套餐返回四个业务值 2. C 端套餐信息返回四个业务值 3. 资产详情页不再混用卡同步流量与套餐业务流量 --- ## 11. 本次改造范围边界 ### 本次必须做 1. 套餐使用记录补快照 2. 同步、扣减、停机、展示统一到套餐层 3. DTO 和接口统一四个业务值 4. 资产流量视角切换为套餐视角 ### 本次不建议强行做 1. 直接重命名数据库旧字段 2. 在本轮立即物理删除所有历史字段 3. 把卡同步辅助字段全部移除 原因: 1. 本次首要目标是统一业务口径,先消除逻辑混乱 2. 物理清理应放在口径稳定后单独做 --- ## 12. 最终结论 本次流量统一改造的核心原则只有两条: 1. **同步看卡,业务看套餐** 2. **展示永远以 `package_usage_id` 为主体,不以资产为主体** 按本方案落地后: 1. 同步链路只负责真流量增量采集 2. 套餐链路负责真已用、停机阈值和四值展示 3. B 端和 C 端最终只消费: - `real_total_mb` - `real_used_mb` - `virtual_total_mb` - `virtual_used_mb` 这套模型可以同时覆盖: 1. 真总量大于虚阈值 2. 真总量小于虚阈值 3. 未启用虚流量 4. 历史套餐快照展示 5. 当前套餐与套餐历史流量统一展示