feat(套餐真流量预警): AUG26-004 真流量预警规则、达量扫描通知与导出

新增 000228 迁移:规则表 tb_package_traffic_alert_rule(每套餐商品至多一条,无软删除,package_id
非部分唯一约束)、达量预警快照表 tb_package_traffic_alert(以主套餐使用记录 + 阈值快照为唯一键,
触发时冻结用量、额度、比例、阈值、到期时间、归属与资产快照),并为 tb_package_usage 新增扫描
范围部分索引 idx_package_usage_alert_scope;down 在预警表存在数据时阻断回滚。

新增规则维护接口 GET/POST/PUT /api/admin/package-traffic-alert-rules(仅超级管理员与平台账号):
创建校验套餐存在且真流量额度大于零,阈值为 1%~100% 的两位小数;修改只影响后续扫描,不回填也
不改写既有预警快照;全部写操作记录操作者、前后值与时间。

新增每日 06:00(Asia/Shanghai)扫描任务 package:traffic:alert:scan,与套餐临期扫描共用 data_cleanup
队列:按资产汇总当前有效套餐的真流量,分子取使用记录真已用量、分母取使用记录真总量快照,命中
主套餐规则阈值时在同一事务创建预警与可靠通知事件;重复执行以唯一冲突视为已处理,不重复投递,
不建停机锁、不调用运营商。

新增预警列表、详情与异步导出 GET /api/admin/package-traffic-alerts、GET /api/admin/package-traffic-alerts/:id、
POST /api/admin/package-traffic-alerts/export,列表与详情一律读冻结快照;新增通知类型
package.traffic.alert 与受控目标 package_traffic_alert_detail,目标解析仅对超级管理员与平台账号
返回可跳转,越权与不存在统一按资源不可见处理。

同步 OpenAPI(cmd/gendocs、cmd/api/docs.go、pkg/openapi/handlers.go)、审计动作与资源注册、上下文
健康检查证据;归档变更并同步 package-traffic-alert 主 Spec。
This commit is contained in:
2026-09-16 17:05:55 +08:00
parent ef4d3696d4
commit d5bcda94fe
46 changed files with 3679 additions and 100 deletions

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-08-31

View File

@@ -0,0 +1,91 @@
## Context
套餐商品 `tb_package.real_data_mb` 是可变配置值(后台可编辑)。套餐使用记录 `tb_package_usage``data_usage_mb`(真已用)与 `data_limit_mb`(真总量快照,购买时写入、全仓无更新路径)承载真实用量;既有 H5、后台资产详情、套餐历史、客户视图、代理开放接口与卡导出一律以使用记录快照展示真额度。现有通知以事件投递并按接收人隔离后台通知无类型白名单但新增通知类型必须先注册模板定义否则渲染失败、通知写不进库。资产数据范围对超级管理员与平台用户为空操作。预警必须是套餐级观测不能复用运营商通道阈值停机锁。
## Goals / Non-Goals
**Goals:**
- 建立套餐真流量规则、预警事实、通知与可追溯导出。
**Non-Goals:**
- 不建停机锁、不调用运营商、不做停复机、不读取通道计费周期与通道累计流量。
## Decisions
### 额度与用量口径
- 分子取 `tb_package_usage.data_usage_mb`(真已用),由卡网关读数增量经既有扣减链路写入。
- 分母取 `tb_package_usage.data_limit_mb`(真总量快照)。该字段与 H5、后台资产详情、套餐历史、客户视图、代理开放接口与卡导出的既有展示一致使用记录快照不可变历史可复现。
- 商品 `real_data_mb` 仅作为规则启用时的配置合法性校验(必须大于零),不作为汇总分母。
- 禁止读取虚流量(`virtual_total_mb_snapshot` / `enable_virtual_data_snapshot`)、展示量(`display_gain_ratio_snapshot` 放大结果)、卡级累计(`tb_iot_card.data_usage_mb`)与运营商通道累计值(`last_gateway_reading_mb``current_month_usage_mb`)。
### 有效套餐集合与主套餐
- 按资产(`iot_card_id``device_id`,二者互斥非零)汇总当前有效套餐使用记录,含加油包:`status IN (1,2) AND refund_id IS NULL`
- 排除 0-待生效、3-已过期、4-已失效。
- 判定只依据 `status`,不得仅用 `expires_at` 判断过期(过期与失效由异步任务改状态,`expires_at` 在生效中状态下可能已到期)。
- 主套餐(阈值来源)为 `master_usage_id IS NULL`;多条时按 `priority ASC, activated_at ASC, id ASC` 取第一条,与既有当前主套餐查询一致。
- 主套餐无有效规则、总额度不大于零或比例未达阈值时跳过。
### 汇总、去重与补建
- 唯一键为 `(package_usage_id, threshold_percent_snapshot)`
- 降阈值后同一使用记录产生新阈值快照的预警属预期补建,不是重复;升阈值或停用不修改既有预警快照。
- 唯一冲突视为已处理,不重复投递通知。
- 实际消耗流量的使用记录所关联资产是权威归属;插拔卡同时展示卡与当前关联设备,不汇总多张卡。
### 规则维护
- `POST /package-traffic-alert-rules`:仅超级管理员、平台用户;请求 `package_id``threshold_percent`(大于等于 1、小于等于 100允许小数`enabled``remark`(最多 500 字符)。套餐必须存在且 `real_data_mb > 0`(配置合法性校验);同套餐已有规则返回“套餐已存在真流量预警规则”。
- `PUT /package-traffic-alert-rules/:id`:允许修改阈值、启停、备注;不修改已产生预警快照。停用后扫描不建新预警;启用或降低阈值后不主动回填,仅由下一次扫描按当前有效套餐判断。
- `GET /package-traffic-alert-rules` 返回套餐、真流量总额度、阈值、启用状态、备注和更新时间。所有成功写操作记录操作者、前后值和时间。
### 扫描与幂等
- Worker 只读取当前有效套餐使用记录(见上集合),按资产聚合真已用量与真总量快照。
- 调度形态照抄既有 `package_expiry_reminder``asynq.Scheduler``CRON_TZ=Asia/Shanghai` 注册,调度器仅在单例 Worker 角色创建。既有临期扫描不使用 `asynq.Unique`,也无启动补偿;本项同样不新增手工触发扫描入口。
- 幂等由事实唯一键(`(package_usage_id, threshold_percent_snapshot)`)与 Outbox 幂等追加(`OnConflict(event_id) DoNothing`)保证。
- 事实与事件在同一事务写入:预警、通知事件与审计同事务提交;唯一冲突视为已处理,不重复投递通知。
- 通知失败进入既有可靠投递恢复,不能删除预警或重新计算快照。
### 通知
- 类别沿用 `expiry`,不新增类别;新增通知类型 `package.traffic.alert`
- 必须注册注册表 `Definition`,否则渲染失败、通知写不进库。
- 接收人新建「仅业务员」解析路径:`business_owner_account_id` 指向启用且 `user_type` 为平台的账号;不得复用返回「店铺 agent 账号 业务员」的既有 resolver。
- 无有效业务员时只保存预警,不补发给未来业务员。
- 新增受控 `ref_type` 并在通知目标定义中注册,指向预警详情,不返回 URL。
- 幂等键内嵌使用记录与阈值快照。
### 列表、详情与导出
- `GET /package-traffic-alerts`:仅超级管理员、平台用户,先应用既有资产数据范围;支持套餐、店铺、业务员、资产/卡标识、阈值、触发时间和通知投递状态筛选、分页。返回冻结快照与通知结果,当前归属变化不得改写预警事实。
- `GET /package-traffic-alerts/:id`:同一数据范围校验后返回完整预警快照和通知投递历史;越权与不存在统一按既有资源不可见处理。
- `POST /package-traffic-alerts/export`:仅超级管理员、平台用户。复用既有异步导出任务机制(新增导出场景并复用导出任务创建服务),场景需在全部五处注册点落地(场景常量、`DataSource`、注册中心实例化、场景白名单、导出 DTO `oneof` 两处);创建时冻结操作者、筛选、时间范围与可见资产范围。既有通用导出入口不做场景级角色校验,故本项以受控入口暴露该场景。
- 数据范围使用导出侧的范围过滤(空范围拒绝),不得使用请求上下文版过滤(空范围语义相反)。
- 导出列:资产类型、资产标识、对应标识符、卡标识、设备类型、设备型号、套餐名称、真流量已用量、真流量额度、比例、阈值快照、到期时间、剩余天数、触发时间、店铺、业务员、用户组、通知投递结果;不含运营商通道列。
- 归属口径按 PRD §2.16 写死:导出中套餐、用量、总量、阈值与到期时间使用触发快照;店铺、业务员、用户组按导出执行时当前归属补充。预警行仍冻结店铺与业务员快照,供列表、详情与追溯;两者口径不同且不得混用。用户组不得写入店铺表,按既有实时推导。
### 数据范围与权限
- 列表、详情与导出仅超级管理员与平台用户。
- 先应用既有资产数据范围;当前对超管或平台无实际过滤,该要求保留为冻结语义与未来放开的前置。
- 越权与不存在统一按资源不可见处理。
- 导出范围在创建时冻结,执行期间归属变化不得扩大范围。
### 与通道阈值严格分离
- 不建停机锁、不调用运营商、不做停复机、不读取通道计费周期与通道累计流量,不触碰既有停复机服务与网关停机调用。
## Risks / Trade-offs
- 流量数据延迟 → 下次扫描补建,不回写已冻结预警。
- 多卡设备 → 以使用记录资产归属聚合,不从当前设备反推流量。
- 扫描并发 → 唯一索引处理同一命中重复创建。
- 分母取快照 → 商品改价后重跑,比例与预警不因商品当前值变化;代价是商品价与预警分母可能不同,属预期。
## Migration Plan
新增成对迁移和索引;隔离环境验证规则启停/降阈值、有效套餐汇总、去重、无业务员、权限导出和 up/down/up。

View File

@@ -0,0 +1,28 @@
## Scope
- 迭代编号:`AUG26-004`
## Why
运营需要在套餐真实流量达量时通知资产所属店铺的有效业务员;现有套餐和通知能力没有规则、去重预警事实或可导出的触发快照。
## What Changes
- 为每个套餐商品维护至多一条 1%100% 真流量预警规则;规则变更只影响后续扫描,既有预警冻结快照。
- 扫描同一资产全部当前有效套餐的真流量汇总,分子取使用记录真已用量、分母取使用记录真总量快照,以实际消耗流量的套餐记录关联资产和主套餐规则判断达量;不读取虚流量、展示量、卡级累计或运营商通道累计值。
- 为同一套餐使用记录和阈值快照仅建一条预警,事务内同时写可靠通知事件,通知当时有效业务员,并提供权限受控列表与异步导出。
- 预警与运营商通道阈值严格分离:不建停机锁、不调用运营商、不做停复机。
## Capabilities
### New Capabilities
- `package-traffic-alert`: 真流量规则、预警事实、通知和导出。
### Modified Capabilities
- 无。既有套餐状态与通知接收人隔离规则保持不变。
## Impact
影响套餐配置、套餐使用/流量扫描任务、资产投影、通知事件、导出、审计和新增 Schema。

View File

@@ -0,0 +1,89 @@
## Purpose
按套餐真实流量和当前有效套餐事实生成一次性达量预警,向资产所属店铺当时有效业务员投递可追溯通知,而不将通道级停复机控制或虚流量混入套餐预警。
## ADDED Requirements
### Requirement: 真流量预警规则
系统 SHALL 为每个套餐商品维护至多一条当前真流量预警规则,阈值为 1% 至 100% 的小数百分比。规则启用、修改或降低阈值只影响后续扫描;既有预警 MUST 保留触发时的套餐、阈值、流量和资产快照。规则停用后停止创建新预警;重新启用或降低阈值后,下次扫描发现已有有效套餐达量时必须补建符合条件的预警。
#### Scenario: 降低阈值后补建
- **WHEN** 管理员降低一个启用规则的阈值,下一次扫描发现其有效套餐已达到新阈值
- **THEN** 系统创建预警并冻结新阈值,不修改既有预警快照
#### Scenario: 套餐无真流量额度不可启用
- **WHEN** 管理员为真流量商品额度不大于零的套餐启用规则
- **THEN** 系统拒绝并保持该套餐无有效规则
### Requirement: 有效套餐与汇总口径
系统 SHALL 以同一资产全部当前有效套餐的真流量汇总比例判断达量,比例分子为套餐使用记录真已用量,分母为套餐使用记录真总量快照,并使用该资产主套餐的规则。当前有效套餐 MUST 为 `status IN (1,2) AND refund_id IS NULL` 的使用记录,含加油包;系统 MUST 排除待生效、已过期与已失效记录,且判定过期 MUST 只依据状态,不得仅用到期时间。实际消耗流量的套餐使用记录所关联资产是权威归属;插拔卡时预警同时展示卡与当前关联设备,但 MUST NOT 汇总多张卡。虚流量、展示量、卡级累计与运营商通道累计值 MUST NOT 计入。主套餐 MUST 为 `master_usage_id IS NULL` 的使用记录,存在多条时按优先级、生效时间、编号依次取第一条。
#### Scenario: 多个有效套餐共同达量
- **WHEN** 某资产的多个当前有效套餐真流量汇总达到其主套餐规则阈值
- **THEN** 系统为命中套餐使用记录创建唯一预警,并只向扫描时该资产所属店铺的有效业务员投递通知
#### Scenario: 商品改价后重跑
- **WHEN** 管理员修改套餐商品的当前真流量配置值,同一使用记录被再次扫描
- **THEN** 汇总比例与预警结果不因商品当前值变化
#### Scenario: 已过期或已失效记录不计入
- **WHEN** 资产的某条套餐使用记录状态为已过期或已失效
- **THEN** 系统不将该记录的真已用量与真总量计入汇总
### Requirement: 去重与补建
系统 SHALL 以套餐使用记录和命中阈值快照的组合作为预警唯一键。同一组合 MUST 至多创建一条预警;唯一冲突 MUST 视为已处理且不重复投递通知。降低阈值后对同一使用记录产生新阈值快照的预警 MUST 视为预期补建,不受既有预警阻塞。
#### Scenario: 重复扫描
- **WHEN** 相同套餐使用记录和相同阈值被重复扫描命中
- **THEN** 系统保留原预警和通知,不创建重复记录
### Requirement: 预警通知
系统 SHALL 在预警创建事务内向资产所属店铺当时有效业务员创建站内通知。预警类别 MUST 沿用 `expiry`,并 MUST 注册通知类型 `package.traffic.alert` 的模板定义。有效业务员 MUST 为店铺 `business_owner_account_id` 指向的启用平台账号,系统 MUST NOT 向店铺代理账号补发。无有效业务员时系统 MUST 只保留预警,且 MUST NOT 补发给未来业务员。通知 MUST 幂等,幂等键 MUST 内嵌使用记录与阈值快照,且 MUST 注册指向预警详情的受控目标,不返回 URL。
#### Scenario: 存在有效业务员
- **WHEN** 资产所属店铺在扫描时存在有效业务员
- **THEN** 系统在同一事务创建预警与一条站内通知,通知在账户侧可见、未读数正确并可跳转受控目标
#### Scenario: 无有效业务员
- **WHEN** 资产所属店铺在扫描时无有效业务员
- **THEN** 系统只保存预警,不创建通知且不向未来业务员补发
### Requirement: 预警查询与导出
超级管理员和平台用户 SHALL 在既有资产数据范围内查询和导出预警;先应用既有资产数据范围,该范围当前对超级管理员与平台无实际过滤,保留为冻结语义与未来放开的前置。越权与不存在 MUST 统一按既有的资源不可见处理。导出 MUST 复用既有异步任务并在创建时冻结操作者、筛选、时间范围与可见资产范围,执行期间归属变化 MUST NOT 扩大范围,且 MUST 按触发时间筛选。列表、详情与导出 MUST 返回资产类型、资产标识、对应标识符、卡标识、设备类型、设备型号、套餐名称、真流量已用量、真流量额度、比例、阈值快照、到期时间、剩余天数、触发时间、店铺、业务员、用户组与通知投递结果,且 MUST NOT 包含运营商通道列。导出中套餐、用量、总量、阈值与到期时间 MUST 使用触发快照,店铺、业务员与用户组 MUST 按执行时当前归属补充;预警行仍冻结店铺与业务员快照供列表、详情与追溯,两种口径 MUST NOT 混用;用户组 MUST NOT 写入店铺表,按既有实时推导。
#### Scenario: 受限导出
- **WHEN** 平台用户在其资产数据范围内创建预警导出
- **THEN** 导出仅包含创建时可见预警,即使任务执行期间店铺归属发生变化
#### Scenario: 归属变更后导出
- **WHEN** 预警记录创建后资产所属店铺或业务员变更,再执行已创建导出任务
- **THEN** 套餐、用量、总量、阈值与到期时间仍使用触发快照,店铺、业务员与用户组使用执行时当前归属,且不超出任务创建时冻结的可见范围
#### Scenario: 越权或不存在
- **WHEN** 调用者读取不在其可见范围内的预警详情
- **THEN** 系统按资源不可见处理,不区分越权、不存在与已删除
### Requirement: 与运营商通道阈值分离
系统 SHALL 将本能力与运营商通道阈值控制严格分离。达量预警 MUST NOT 创建停机锁、调用运营商、执行停复机MUST NOT 读取通道计费周期或通道累计流量。
#### Scenario: 达量预警不触发通道动作
- **WHEN** 系统为某资产创建真流量达量预警
- **THEN** 系统不写入任何停机锁、不调用运营商接口且不改变卡停机状态

View File

@@ -0,0 +1,28 @@
## 1. 数据与规则
- [x] 1.1 追踪套餐使用有效态、真流量字段、资产/卡/设备关联、有效业务员、通知事件及异步导出调用链。
- [x] 1.2 新增成对迁移、模型和约束:套餐唯一规则、预警快照、使用记录+阈值快照唯一去重、查询/导出索引。
- [x] 1.3 实现规则 CRUD、1%100% 校验(分母为使用记录真总量快照,规则启用校验商品真流量额度大于零)、启停和审计;更新套餐管理 OpenAPI。
## 2. 扫描与通知
- [x] 2.1 实现可重跑扫描:按资产汇总当前有效套餐(`status IN (1,2) AND refund_id IS NULL`)真流量,分子取使用记录真已用量、分母取使用记录真总量快照,排除虚流量/展示量/卡级累计/通道累计与失效记录。
- [x] 2.2 选择主套餐规则(`master_usage_id IS NULL`,多条按 `priority ASC, activated_at ASC, id ASC` 取第一条),主套餐无规则或总额度不大于零时跳过。
- [x] 2.3 原子创建预警(唯一键 `(package_usage_id, threshold_percent_snapshot)`)与通知事件;事实与事件同一事务,唯一冲突视为已处理。
- [x] 2.4 接入既有任务调度(照抄临期扫描的 Cron 加单例注册,不使用 `asynq.Unique`、不新增启动补偿、不新增手工触发入口)与既有通知投递。
- [x] 2.5 注册通知类型常量 `package.traffic.alert`、注册表 Definition类别沿用 `expiry`)、仅业务员解析路径(`business_owner_account_id` 指向启用平台账号)与受控目标定义;无有效业务员只建预警、幂等键内嵌使用记录与阈值快照。
## 3. 读侧与导出
- [x] 3.1 实现受资产数据范围保护的预警列表/详情(仅超管与平台,越权与不存在统一不可见)。
- [x] 3.2 按 PRD §2.16 落地导出列:资产类型、资产标识、对应标识符、卡标识、设备类型、设备型号、套餐名称、真流量已用量、真流量额度、比例、阈值快照、到期时间、剩余天数、触发时间、店铺、业务员、用户组、通知投递结果(不含运营商通道列)。
- [x] 3.3 在五处注册点接入导出场景:场景常量、`DataSource` 实现、注册中心实例化、场景白名单、导出 DTO `oneof` 两处;表头在 dispatch 阶段冻结。
- [x] 3.4 导出使用导出侧范围过滤(空范围拒绝,不得用请求上下文版);创建时冻结操作者、筛选、时间范围与可见资产范围;流量与阈值用触发快照,店铺/业务员/用户组按执行时当前归属补充,用户组按既有实时推导。
## 4. 验证
- [x] 4.1 在隔离数据库验证迁移 up/down/up、规则启停/降阈值补建、有效套餐汇总、重复扫描、插拔卡展示、通知接收人和导出权限。
- [x] 4.2 验证商品改价后重跑比例与预警不因商品当前值变化;已过期或已失效记录不计入。
- [x] 4.3 验证唯一冲突不重复通知、无有效业务员只建预警、通知在账户侧可见/未读正确/可跳转受控目标。
- [x] 4.4 验证导出归属为执行时当前归属、流量与阈值为触发快照且不超创建时范围;越权与不存在统一不可见;数据范围冻结语义成立。
- [x] 4.5 运行 `gofmt -w``go build ./cmd/api ./cmd/worker``go run cmd/gendocs/main.go``openspec validate add-package-real-usage-alerts --strict``openspec doctor --json``./scripts/context-health.sh`;自动化测试按项目决策为 N/A。