This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-04-27
|
||||
164
openspec/changes/refactor-traffic-model-semantics/design.md
Normal file
164
openspec/changes/refactor-traffic-model-semantics/design.md
Normal file
@@ -0,0 +1,164 @@
|
||||
## Context
|
||||
|
||||
当前仓库中的流量语义分散在三层:
|
||||
|
||||
1. **同步层**:`tb_iot_card.last_gateway_reading_mb`、`current_month_usage_mb`、`data_usage_mb` 负责承接上游累计量与增量。
|
||||
2. **套餐层**:`tb_package_usage.data_limit_mb`、`data_usage_mb` 负责真实使用量累计、主套餐/加油包优先级扣减、状态流转与停复机联动。
|
||||
3. **展示层**:后台 `asset-resolve / current-package / packages`、C 端 `asset/info / package-history`、客户视图流量接口,又额外暴露了 `package_total_mb`、`package_used_mb`、`virtual_limit_mb`、`virtual_ratio` 等旧字段,且不同接口对 ratio 有“乘”和“除”两种口径。
|
||||
|
||||
本次变更不是新增一个流量接口,而是把“真实消耗事实”“停机阈值”“前端展示映射”三件事拆开并统一命名。已确认的业务共识如下:
|
||||
|
||||
- `virtual_total_mb` 永远大于 0 且小于等于 `real_total_mb`
|
||||
- 一个载体同一时刻只有一个生效主套餐,可以存在多个生效/已用完的加油包
|
||||
- 对外必须统一返回:`real_total_mb`、`real_used_mb`、`virtual_total_mb`、`virtual_used_mb`、`reduction_pct`
|
||||
- `virtual_used_mb` 不是简单的“缩小后的已用量”,而是把真已用映射回真总量刻度后的展示值
|
||||
- 旧的模糊字段最终要退出,避免继续误导后续开发和前端消费方
|
||||
|
||||
当前约束:
|
||||
|
||||
- 保持 Handler → Service → Store → Model 分层,不引入新依赖
|
||||
- 数据库仍使用 GORM 与手动 ID 关联,不使用外键 / GORM 关联标签
|
||||
- 停复机、轮询、扣减、展示接口都要共用同一套语义,不能只改 DTO 名称
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- 统一套餐流量事实字段,明确 `tb_package_usage` 才是业务事实源
|
||||
- 为套餐使用记录补充虚总量与展示映射快照,避免历史展示继续读取 live `tb_package`
|
||||
- 统一 `virtual_used_mb` 与 `reduction_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_mb` 与 `virtual_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` 与资产摘要类接口只代表当前生效主套餐
|
||||
- `packages`、`package-history` 代表按 `package_usage_id` 组织的套餐记录视图
|
||||
- 主套餐与加油包共存关系继续通过 `master_usage_id` 与 `priority` 表达,不引入新的关系模型
|
||||
- `asset-resolve` 与 `asset/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` 的空结果契约。
|
||||
@@ -0,0 +1,38 @@
|
||||
## Why
|
||||
|
||||
feature-001-traffic-model-refactor:当前项目的流量语义分散在卡同步字段、套餐使用字段和 B/C 端展示字段三套口径中,`virtual_ratio`、`package_total_mb`、`virtual_limit_mb` 等命名也持续放大理解成本。现在业务已经明确新的统一口径:虚总量永远不大于真总量、一个载体同一时刻只有一个生效主套餐但可挂多个加油包、对外必须同时提供真值/虚值/展示换算值,因此需要正式发起一次流量模型重构,而不是继续在旧字段上叠补丁。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 新增统一的流量业务语义,明确 `package_usage` 为套餐流量事实源,对外统一为真总量、真已用、虚总量、虚已用和 `reduction_pct`(缩减比例)字段。
|
||||
- 明确虚流量约束:`virtual_total_mb` 必须大于 0 且小于等于 `real_total_mb`;废弃现有允许纯虚流量或虚总量大于真总量的旧约定。 **BREAKING**
|
||||
- 统一虚已用展示公式,停止在不同接口里混用“除以 ratio”和“乘以 ratio”的实现。 **BREAKING**
|
||||
- 将停机判断从“按 `data_limit_mb/data_usage_mb` 的旧直觉口径”收束为“按套餐使用记录上的真值/虚阈值语义”统一判断。 **BREAKING**
|
||||
- 统一主套餐 / 加油包流量展示模型:`current-package` 仅表示当前生效主套餐,套餐历史与 C 端套餐列表按 `package_usage_id` 逐条展示各套餐用量。
|
||||
- 清理并替换误导性旧字段,包含但不限于 `package_total_mb`、`package_used_mb`、`package_remain_mb`、`virtual_limit_mb`、`virtual_remain_mb`、旧式 `virtual_ratio` 展示语义,并在本次变更中一并删除确认废弃的数据库旧列。 **BREAKING**
|
||||
- 为历史套餐使用记录补充必要快照字段,避免继续依赖 `tb_package` 当前值回放历史流量展示。
|
||||
- 同步更新 OpenAPI / DTO / 文档 / 手工验收口径,确保 Handler → Service → Store → Model 各层使用相同字段定义。
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `traffic-model-semantics`: 定义统一的套餐流量事实字段、展示字段、比例字段、停机阈值语义及历史快照规则。
|
||||
|
||||
### Modified Capabilities
|
||||
- `iot-package`: 修改套餐真流量/虚流量定义、校验规则、`virtual_ratio` 使用语义及展示说明。
|
||||
- `asset-resolve`: 修改资产解析接口中的套餐流量字段与展示计算规则。
|
||||
- `asset-queries`: 修改后台套餐历史与当前主套餐接口的流量字段、当前主套餐语义与错误处理约定。
|
||||
- `client-asset-info`: 修改 C 端资产信息与套餐历史接口的流量响应结构和展示口径。
|
||||
- `package-usage-priority`: 修改按优先级扣减后的耗尽判断与套餐使用字段语义。
|
||||
- `auto-stop-resume`: 修改自动停复机的流量耗尽判定基础口径。
|
||||
|
||||
## Impact
|
||||
|
||||
- **分层影响**:
|
||||
- Handler:`internal/handler/admin/asset.go`、`internal/handler/app/client_asset.go` 等返回结构与错误语义需更新。
|
||||
- Service:`internal/service/asset/service.go`、`internal/service/package/usage_service.go`、`internal/service/iot_card/stop_resume_service.go`、`internal/task/polling_carddata_handler.go` 等需统一流量计算口径。
|
||||
- Store / Model:`internal/model/package.go`、相关 DTO、`tb_package_usage` 快照字段与查询语义需要调整。
|
||||
- **API / 文档影响**:后台 `resolve / current-package / packages`、C 端 `asset/info / package-history` 及相关 OpenAPI 文档都会产生字段级 breaking change。
|
||||
- **数据影响**:需要为 `tb_package_usage` 做历史回填、快照迁移和旧列删除,避免历史展示继续读取 live `tb_package` 值或继续依赖废弃字段。
|
||||
- **验证计划**:本项目执行阶段以 PostgreSQL 手工核验、接口回放、网关同步链路验收为主,不在本提案内引入新的自动化测试要求。
|
||||
- **性能考虑**:保持现有分页与查询边界,不引入新的重型聚合查询;关键接口仍需满足列表默认 20/最大 100、API P95 < 200ms、数据库查询 < 50ms 的现有约束。
|
||||
@@ -0,0 +1,53 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 套餐历史列表查询
|
||||
系统 SHALL 提供资产的全量套餐记录查询接口,包含历史和当前生效套餐。
|
||||
|
||||
**API 端点**: `GET /api/admin/assets/:asset_type/:id/packages`
|
||||
|
||||
**每条记录响应字段**:
|
||||
- `package_usage_id`
|
||||
- `package_name`
|
||||
- `package_type`
|
||||
- `master_usage_id`
|
||||
- `real_total_mb`
|
||||
- `real_used_mb`
|
||||
- `virtual_total_mb`
|
||||
- `virtual_used_mb`
|
||||
- `reduction_pct`
|
||||
- `activated_at`
|
||||
- `expires_at`
|
||||
- `status`
|
||||
- `paid_amount`
|
||||
|
||||
**兼容规则**:
|
||||
- 不再返回 `real_data_mb`、`virtual_data_mb`、`package_used_mb`、`package_remain_mb` 等旧展示字段
|
||||
|
||||
#### Scenario: 历史列表返回统一五字段
|
||||
- **WHEN** 管理员调用 `GET /api/admin/assets/card/123/packages`
|
||||
- **THEN** 每条套餐记录都返回统一五字段和 `reduction_pct`
|
||||
|
||||
#### Scenario: 无套餐历史时返回空数组
|
||||
- **WHEN** 管理员查询一张从未购买套餐的卡
|
||||
- **THEN** 系统返回空数组,不报错
|
||||
|
||||
### Requirement: 当前主套餐详情查询
|
||||
系统 SHALL 提供查询资产当前生效主套餐的接口,用于展示主套餐详细信息。
|
||||
|
||||
**API 端点**: `GET /api/admin/assets/:asset_type/:id/current-package`
|
||||
|
||||
**查询条件**: `status = 1 AND master_usage_id IS NULL`
|
||||
|
||||
**响应字段**:
|
||||
- 与套餐历史列表中的单条记录字段一致
|
||||
- 不包含加油包汇总
|
||||
- 无生效主套餐时返回 HTTP 200,且 `data = null`
|
||||
|
||||
#### Scenario: 当前套餐只返回主套餐
|
||||
- **WHEN** 管理员调用 `GET /api/admin/assets/card/123/current-package`,该卡存在 1 个生效主套餐和多个加油包
|
||||
- **THEN** 系统只返回主套餐的统一五字段与 `reduction_pct`
|
||||
|
||||
#### Scenario: 无当前主套餐返回 200 + null
|
||||
- **WHEN** 管理员查询没有生效中主套餐的资产
|
||||
- **THEN** 系统返回 HTTP 200
|
||||
- **AND** 响应体中的 `data = null`
|
||||
@@ -0,0 +1,29 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 统一资产解析入口
|
||||
系统 SHALL 提供统一的资产查找接口,通过任意标识符定位卡或设备,并返回该资产的中等聚合信息。
|
||||
|
||||
**API 端点**: `GET /api/admin/assets/resolve/:identifier`
|
||||
|
||||
**流量相关响应结构(AssetResolveResponse)**:
|
||||
- `current_package`: 当前主套餐名称(无主套餐时返回空字符串)
|
||||
- `current_package_usage_id`: 当前主套餐的套餐使用记录 ID(无主套餐时为 null)
|
||||
- `real_total_mb`: 当前主套餐真实总量
|
||||
- `real_used_mb`: 当前主套餐真实已用量
|
||||
- `virtual_total_mb`: 当前主套餐业务停机阈值
|
||||
- `virtual_used_mb`: 当前主套餐展示已用量
|
||||
- `reduction_pct`: 按 `(real_total_mb / virtual_total_mb) - 1` 计算
|
||||
|
||||
**流量展示规则**:
|
||||
- 流量摘要只代表当前生效主套餐,不聚合加油包
|
||||
- 无当前生效主套餐时,上述流量字段全部返回 0,`current_package_usage_id` 返回 null
|
||||
|
||||
#### Scenario: 返回当前主套餐五字段摘要
|
||||
- **WHEN** 管理员解析一张存在生效主套餐的卡
|
||||
- **THEN** 响应中包含 `current_package_usage_id`、`real_total_mb`、`real_used_mb`、`virtual_total_mb`、`virtual_used_mb` 和 `reduction_pct`
|
||||
|
||||
#### Scenario: 无主套餐时返回空摘要
|
||||
- **WHEN** 管理员解析一台没有当前生效主套餐的设备
|
||||
- **THEN** 响应中 `current_package = ""`
|
||||
- **AND** `current_package_usage_id = null`
|
||||
- **AND** 流量字段均返回 `0`
|
||||
@@ -0,0 +1,27 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 流量耗尽自动停机
|
||||
系统 SHALL 在主套餐和所有加油包都达到各自业务耗尽阈值后,调用运营商接口停机。
|
||||
|
||||
**业务耗尽阈值**:
|
||||
- 启用虚流量:`real_used_mb >= virtual_total_mb`
|
||||
- 未启用虚流量:`real_used_mb >= real_total_mb`
|
||||
|
||||
#### Scenario: 主套餐达到虚阈值触发停机
|
||||
- **WHEN** 卡的当前主套餐 `real_total_mb=100`、`virtual_total_mb=70`,并且 `real_used_mb=70`
|
||||
- **THEN** 系统将该主套餐视为耗尽并进入停机检查
|
||||
|
||||
#### Scenario: 加油包仍有业务额度时不停机
|
||||
- **WHEN** 主套餐已达到业务阈值,但仍存在加油包 `real_used_mb < effective_threshold_mb`
|
||||
- **THEN** 系统不触发停机
|
||||
|
||||
### Requirement: 购买套餐自动复机
|
||||
系统 SHALL 在存在 active 套餐且该套餐仍有业务剩余额度时,自动触发复机。
|
||||
|
||||
#### Scenario: 购买加油包后恢复可用额度则自动复机
|
||||
- **WHEN** 卡已因 `traffic_exhausted` 停机,随后新增一条 active 加油包,且该加油包 `real_used_mb < effective_threshold_mb`
|
||||
- **THEN** 系统自动调用运营商复机接口
|
||||
|
||||
#### Scenario: 仅存在已用完套餐时不复机
|
||||
- **WHEN** 卡处于停机状态,所有 active 或 depleted 套餐都满足 `real_used_mb >= effective_threshold_mb`
|
||||
- **THEN** 系统不触发复机
|
||||
@@ -0,0 +1,46 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: B1 资产基本信息查询接口
|
||||
系统 SHALL 提供 `GET /api/c/v1/asset/info?identifier=xxx`,并且 MUST 要求个人客户认证(C 端 Token)。
|
||||
|
||||
**流量相关响应字段**:
|
||||
- `current_package`
|
||||
- `current_package_usage_id`
|
||||
- `real_total_mb`
|
||||
- `real_used_mb`
|
||||
- `virtual_total_mb`
|
||||
- `virtual_used_mb`
|
||||
- `reduction_pct`
|
||||
|
||||
**约束**:
|
||||
- 流量摘要仅表示当前主套餐,不聚合加油包
|
||||
- 不再返回 `package_total_mb`、`package_used_mb`、`package_remain_mb` 等旧字段
|
||||
|
||||
#### Scenario: C 端资产信息返回主套餐五字段
|
||||
- **WHEN** 客户调用 `GET /api/c/v1/asset/info?identifier=8986xxxx` 且资产存在当前主套餐
|
||||
- **THEN** 响应包含当前主套餐的统一五字段与 `reduction_pct`
|
||||
|
||||
#### Scenario: 无主套餐时返回空摘要
|
||||
- **WHEN** 客户查询的资产没有当前主套餐
|
||||
- **THEN** 响应中流量字段返回 `0`,`current_package_usage_id` 返回 `null`
|
||||
|
||||
### Requirement: B3 历史套餐列表接口
|
||||
系统 SHALL 提供 `GET /api/c/v1/asset/package-history?identifier=xxx&page=1&page_size=20`,并且 MUST 要求个人客户认证。
|
||||
|
||||
**列表项字段**:
|
||||
- `package_usage_id`
|
||||
- `package_name`
|
||||
- `package_type`
|
||||
- `master_usage_id`
|
||||
- `real_total_mb`
|
||||
- `real_used_mb`
|
||||
- `virtual_total_mb`
|
||||
- `virtual_used_mb`
|
||||
- `reduction_pct`
|
||||
- `activated_at`
|
||||
- `expires_at`
|
||||
- `status`
|
||||
|
||||
#### Scenario: C 端历史列表复用统一字段
|
||||
- **WHEN** 客户调用 `GET /api/c/v1/asset/package-history`
|
||||
- **THEN** 列表项使用统一五字段与 `reduction_pct`,不再混用旧展示字段
|
||||
@@ -0,0 +1,39 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 套餐实体定义
|
||||
系统 SHALL 定义套餐(Package)实体,包含套餐的基本属性、定价、流量配置,以及用于展示映射的内部比例字段。
|
||||
|
||||
**实体字段**:
|
||||
- `real_data_mb`: 真流量额度(MB,套餐真实总量)
|
||||
- `virtual_data_mb`: 虚流量额度(MB,业务停机阈值,必须大于 0 且小于等于 `real_data_mb`)
|
||||
- `virtual_ratio`: 内部展示映射比例,定义为 `real_data_mb / virtual_data_mb`
|
||||
- `enable_virtual_data`: 是否启用虚流量(未启用时 `virtual_ratio = 1.0`)
|
||||
|
||||
**约束**:
|
||||
- `enable_virtual_data = true` 时,`virtual_data_mb` MUST 大于 0
|
||||
- `enable_virtual_data = true` 时,`virtual_data_mb` MUST 小于等于 `real_data_mb`
|
||||
- `virtual_ratio` 由 Service 层自动计算并存储,不由调用方传入
|
||||
|
||||
#### Scenario: 创建启用虚流量的套餐
|
||||
- **WHEN** 平台创建套餐,`real_data_mb=100`、`virtual_data_mb=70`、`enable_virtual_data=true`
|
||||
- **THEN** 系统自动计算并存储 `virtual_ratio = 100 / 70 ≈ 1.428571`
|
||||
|
||||
#### Scenario: 创建虚流量大于真流量的套餐失败
|
||||
- **WHEN** 平台创建套餐,`real_data_mb=100`、`virtual_data_mb=120`、`enable_virtual_data=true`
|
||||
- **THEN** 系统拒绝保存并返回参数错误
|
||||
|
||||
### Requirement: 套餐流量类型和真虚流量共存
|
||||
系统 SHALL 支持同一套餐同时定义真实总量和业务阈值,但 `virtual_data_mb` 不再表示与真流量相加的“另一份总量”,而是展示映射与停机判断所依赖的阈值。
|
||||
|
||||
**流量类型定义**:
|
||||
- **真流量(real_data_mb)**: 套餐真实总量
|
||||
- **虚流量(virtual_data_mb)**: 业务停机阈值和展示映射基准
|
||||
- **未启用虚流量**: `virtual_data_mb` 视为等于 `real_data_mb`
|
||||
|
||||
#### Scenario: 真总量与虚阈值并存
|
||||
- **WHEN** 平台创建套餐,`real_data_mb=100`、`virtual_data_mb=70`
|
||||
- **THEN** 系统将其解释为“真实总量 100、业务阈值 70”,而不是“总量 170”
|
||||
|
||||
#### Scenario: 不允许纯虚流量套餐
|
||||
- **WHEN** 平台尝试创建 `real_data_mb=0` 且 `virtual_data_mb>0` 的套餐
|
||||
- **THEN** 系统拒绝保存并返回参数错误
|
||||
@@ -0,0 +1,22 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 流量优先扣减加油包
|
||||
系统 SHALL 在扣减流量时优先扣减加油包,再扣减主套餐;每条套餐的剩余额度 SHALL 基于业务耗尽阈值而不再直接等同于 `real_total_mb`。
|
||||
|
||||
**业务阈值定义**:
|
||||
- 启用虚流量:`effective_threshold_mb = virtual_total_mb_snapshot`
|
||||
- 未启用虚流量:`effective_threshold_mb = real_total_mb`
|
||||
|
||||
**扣减规则**:
|
||||
- `real_used_mb` 继续累计真实用量
|
||||
- `remaining_quota_mb = effective_threshold_mb - real_used_mb`
|
||||
- 当 `real_used_mb >= effective_threshold_mb` 时,套餐状态更新为已用完
|
||||
|
||||
#### Scenario: 启用虚流量的加油包按虚阈值耗尽
|
||||
- **WHEN** 某加油包 `real_total_mb=100`、`virtual_total_mb=70`、当前 `real_used_mb=68`,本次需要继续扣减 2MB
|
||||
- **THEN** 系统将该加油包扣减到 `real_used_mb=70`
|
||||
- **AND** 将其状态更新为已用完
|
||||
|
||||
#### Scenario: 所有 active 套餐均达到业务阈值后触发停机检查
|
||||
- **WHEN** 主套餐和所有加油包都满足 `real_used_mb >= effective_threshold_mb`
|
||||
- **THEN** 系统进入停机判断流程
|
||||
@@ -0,0 +1,56 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 套餐流量对外语义统一
|
||||
系统 SHALL 以套餐使用记录为事实源,对外统一返回 `real_total_mb`、`real_used_mb`、`virtual_total_mb`、`virtual_used_mb` 和 `reduction_pct`,不再要求调用方从旧字段或 live `tb_package` 值自行拼接流量语义。
|
||||
|
||||
**字段语义**:
|
||||
- `real_total_mb`: 套餐真实总量
|
||||
- `real_used_mb`: 套餐真实已用量
|
||||
- `virtual_total_mb`: 套餐业务停机阈值
|
||||
- `virtual_used_mb`: 将 `real_used_mb` 映射到 `real_total_mb` 刻度后的展示值
|
||||
- `reduction_pct`: 默认按 `(real_total_mb / virtual_total_mb) - 1` 计算,字段名继续沿用业务口语“缩减比例”,接口返回值使用 `0.428571` 这种比例小数格式
|
||||
|
||||
#### Scenario: 启用虚流量时返回统一五字段
|
||||
- **WHEN** 某条套餐使用记录的 `real_total_mb=100`、`real_used_mb=50`、`virtual_total_mb=70`
|
||||
- **THEN** 系统返回 `virtual_used_mb=71.43`
|
||||
- **AND** 同时返回 `reduction_pct=0.428571...`
|
||||
|
||||
#### Scenario: 未启用虚流量时退化为真流量视图
|
||||
- **WHEN** 某条套餐使用记录未启用虚流量
|
||||
- **THEN** 系统返回 `virtual_total_mb = real_total_mb`
|
||||
- **AND** `virtual_used_mb = real_used_mb`
|
||||
- **AND** `reduction_pct=0`
|
||||
|
||||
### Requirement: 虚已用展示公式统一
|
||||
系统 SHALL 使用统一公式计算 `virtual_used_mb`:`min(real_used_mb * (real_total_mb / virtual_total_mb), real_total_mb)`。
|
||||
|
||||
#### Scenario: 真已用接近虚阈值时展示接近满量程
|
||||
- **WHEN** `real_total_mb=100`、`virtual_total_mb=70`、`real_used_mb=69`
|
||||
- **THEN** 系统返回 `virtual_used_mb=98.57`
|
||||
|
||||
#### Scenario: 真已用达到虚阈值时展示封顶
|
||||
- **WHEN** `real_total_mb=100`、`virtual_total_mb=70`、`real_used_mb=70`
|
||||
- **THEN** 系统返回 `virtual_used_mb=100`
|
||||
|
||||
### Requirement: 套餐耗尽阈值统一
|
||||
系统 SHALL 将套餐业务耗尽阈值统一定义为:启用虚流量时使用 `virtual_total_mb`,未启用虚流量时使用 `real_total_mb`。
|
||||
|
||||
#### Scenario: 启用虚流量时按虚阈值耗尽
|
||||
- **WHEN** 某条套餐使用记录 `real_total_mb=100`、`virtual_total_mb=70`、`real_used_mb=70`
|
||||
- **THEN** 系统将该套餐判定为已用完
|
||||
|
||||
#### Scenario: 未启用虚流量时按真总量耗尽
|
||||
- **WHEN** 某条套餐使用记录未启用虚流量且 `real_used_mb=real_total_mb`
|
||||
- **THEN** 系统将该套餐判定为已用完
|
||||
|
||||
### Requirement: 历史套餐流量快照可回放
|
||||
系统 SHALL 在 `tb_package_usage` 上持久化虚总量与展示映射所需快照,以保证历史流量展示不继续依赖 live `tb_package` 值。
|
||||
|
||||
#### Scenario: 创建新套餐使用记录时写入快照
|
||||
- **WHEN** 系统创建新的 `PackageUsage`
|
||||
- **THEN** 同步写入 `virtual_total_mb_snapshot`、展示倍率快照和虚流量开关快照
|
||||
|
||||
#### Scenario: 历史套餐记录执行一次性回填
|
||||
- **WHEN** 本次变更上线执行历史数据迁移
|
||||
- **THEN** 系统为已有 `PackageUsage` 回填虚总量与展示倍率快照
|
||||
- **AND** 后续读链路优先读取快照值而不是 `tb_package` 当前值
|
||||
34
openspec/changes/refactor-traffic-model-semantics/tasks.md
Normal file
34
openspec/changes/refactor-traffic-model-semantics/tasks.md
Normal file
@@ -0,0 +1,34 @@
|
||||
## 0. 验收基线准备
|
||||
|
||||
- [x] 0.1 梳理后台 `asset-resolve / current-package / packages` 与 C 端 `asset/info / package-history` 的现有字段清单,并整理旧字段到新字段的替换矩阵,验证点包括逐接口比对返回 JSON 与 OpenAPI 文档。
|
||||
- [x] 0.2 固化本次变更的手工验收基线:准备 PostgreSQL 核验 SQL、典型套餐样例(如 100/70/50→71.43、100/70/69→98.57)、接口回放样例,验证点包括所有样例都能复算出一致结果。
|
||||
|
||||
## 1. 套餐流量语义与快照模型
|
||||
|
||||
- [x] 1.1 为 `tb_package_usage` 设计并新增虚总量/展示倍率/开关快照字段迁移,包含历史回填脚本,验证点包括迁移执行后新列存在、默认值正确、历史记录完成回填。
|
||||
- [x] 1.2 更新 `internal/model/package.go` 与相关 DTO/注释,统一 `data_limit_mb=data real_total`、`data_usage_mb=real_used` 的内部语义,验证点包括模型字段、中文注释与 OpenSpec 设计文档一致。
|
||||
- [x] 1.3 收紧套餐创建/更新校验,确保 `virtual_data_mb > 0 且 <= real_data_mb`,并同步统一内部展示倍率计算逻辑,验证点包括创建/更新接口对非法样例拒绝、合法样例正确落库。
|
||||
|
||||
## 2. 写链路与耗尽阈值收口
|
||||
|
||||
- [x] 2.1 修改订单购包、自动购包、主套餐激活/排队激活路径,确保新建 `PackageUsage` 时同时写入虚总量与展示倍率快照,验证点包括主套餐、加油包、待实名激活三条路径的落库字段完整。
|
||||
- [x] 2.2 重构 `package/usage_service` 的扣减逻辑,统一按 `effective_threshold_mb` 计算剩余额度和已用完状态,同时继续保持“加油包优先、主套餐兜底”,验证点包括 100/70 阈值样例和多加油包优先级样例都符合预期。
|
||||
- [x] 2.3 重构 `stop_resume_service`、轮询触发和重置路径,统一按虚阈值/真总量判断耗尽与复机条件,验证点包括“主套餐达到虚阈值停机”“加油包仍有额度不停机”“新增有效加油包后自动复机”三类场景。
|
||||
|
||||
## 3. 后台读链路与契约切换
|
||||
|
||||
- [x] 3.1 重构后台 `asset-resolve` 的流量摘要组装,仅返回当前主套餐的统一五字段与 `reduction_pct`,并返回 `current_package_usage_id`,验证点包括无主套餐返回空摘要、有主套餐返回新字段且不再混入旧字段。
|
||||
- [x] 3.2 重构后台 `current-package` 与 `packages` 的 DTO 和 Service 组装逻辑,按 `package_usage_id` 返回统一五字段与 `reduction_pct`,并将无主套餐场景统一为 `200 + null`,验证点包括主套餐详情仅返回主套餐、历史列表包含主套餐和加油包的新字段集。
|
||||
- [x] 3.3 更新后台 OpenAPI / 文档生成产物,移除 `package_total_mb`、`package_used_mb`、`virtual_limit_mb` 等旧字段描述,验证点包括 `docs/admin-openapi.yaml` 与新 DTO 一致且无遗留字段说明。
|
||||
|
||||
## 4. C 端切换与范围约束
|
||||
|
||||
- [x] 4.1 重构 C 端 `asset/info` 的流量摘要字段,只返回当前主套餐的统一五字段、`reduction_pct` 与 `current_package_usage_id`,验证点包括旧字段移除后接口返回结构与 spec 一致。
|
||||
- [x] 4.2 重构 C 端 `package-history` 的列表项字段,统一为 `package_usage_id + 五字段 + 比例字段`,验证点包括分页、generation 过滤和历史套餐展示都使用新语义。
|
||||
- [x] 4.3 明确 customer-view total 聚合不属于本次 contract 收口范围,确保本次实现不新增 `total.virtual_used_mb` 或等价聚合字段,验证点包括 change 范围内无新 total 聚合契约、无相关 DTO 改造任务。
|
||||
|
||||
## 5. 旧字段清理与最终验收
|
||||
|
||||
- [x] 5.1 删除后台/C 端读链路中对 `package_total_mb`、`package_used_mb`、`package_remain_mb`、`virtual_limit_mb`、`virtual_remain_mb`、旧式 `virtual_ratio` 展示语义以及确认废弃数据库旧列的依赖,验证点包括代码搜索结果不再存在这些旧展示字段的组装逻辑,迁移后数据库中对应旧列已被删除。
|
||||
- [x] 5.2 更新 `docs/traffic-model-unification/流量模型统一改造方案.md`、相关功能总结和提案说明文档,使文字描述、公式和 DTO 名称与本次 OpenSpec 完全一致,验证点包括文档中的示例与接口返回公式一致。
|
||||
- [ ] 5.3 执行最终手工验收:PostgreSQL 核对快照回填、轮询增量入套餐、虚已用公式、停复机逻辑和 B/C 端接口返回;验证点包括全部样例通过且无旧字段残留在最终 contract 中。
|
||||
Reference in New Issue
Block a user