# 流量模型统一改造方案

## 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. 当前套餐与套餐历史流量统一展示
