Files
junhong_cmp_fiber/migrations/000228_add_package_traffic_alert.up.sql
break d5bcda94fe 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。
2026-09-16 17:05:55 +08:00

126 lines
11 KiB
SQL
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- 套餐真流量达量预警:规则表与预警快照表,以及扫描用的有效套餐部分索引。
-- 背景:运营需要按「同一资产全部当前有效套餐的真流量汇总」判断达量,并向触发时资产所属店铺的
-- 有效平台业务员投递站内通知。既有事实里没有任何承载「套餐商品真流量预警规则」的表,
-- 也没有可冻结触发阈值、汇总用量、归属与资产展示快照的预警表,因此本迁移新增两张表;
-- 既有 tb_notification 的类别 CHECK 已允许 expiry通知类型与受控资源类型只在代码注册表登记
-- 故不改通知表结构tb_package_usage 只新增一个非唯一部分索引,不改任何既有列与数据。
--
-- 设计选择:
-- 1. 规则表不设 deleted_at本能力只提供创建、修改阈值与启停没有删除入口
-- 因此 package_id 可以直接用「非部分」唯一约束表达「每个套餐商品至多一条当前规则」,
-- 避免部分唯一索引在 OnConflict 未声明谓词时的失败KNOWN-ISSUE-001
-- 2. 阈值用 NUMERIC(5,2) 表示 1%100% 的小数百分比;判定在应用层用整数基点比较,不使用浮点。
-- 3. 预警表以「主套餐使用记录 + 阈值快照」为唯一键:一个资产的一个阈值至多一条预警,
-- 重复扫描由唯一冲突视为已处理;降低阈值产生新阈值快照属预期补建,不修改既有预警快照。
-- 4. 触发快照列(用量、额度、比例、阈值、到期时间、资产标识、卡标识、对端标识、设备类型与型号、
-- 店铺与业务员)在创建预警时冻结,列表、详情与导出一律读快照,不随之后的归属或绑定变化改写。
-- 5. notification_event_id 只在写入可靠通知事件时填充;无有效业务员时保持为空字符串,
-- 表示该预警不产生通知且不在未来补发。
-- 6. 不使用数据库外键;资产、店铺、账号与规则均以 ID 保存并由应用层显式校验。
CREATE TABLE tb_package_traffic_alert_rule (
id BIGSERIAL PRIMARY KEY,
package_id BIGINT NOT NULL,
threshold_percent NUMERIC(5, 2) NOT NULL,
enabled SMALLINT NOT NULL DEFAULT 0,
remark VARCHAR(500) NOT NULL DEFAULT '',
creator BIGINT NOT NULL DEFAULT 0,
updater BIGINT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uq_package_traffic_alert_rule_package UNIQUE (package_id),
CONSTRAINT ck_package_traffic_alert_rule_threshold CHECK (threshold_percent >= 1 AND threshold_percent <= 100),
CONSTRAINT ck_package_traffic_alert_rule_enabled CHECK (enabled IN (0, 1))
);
COMMENT ON TABLE tb_package_traffic_alert_rule IS '套餐真流量预警规则,每个套餐商品至多一条当前规则,仅超级管理员与平台账号维护';
COMMENT ON COLUMN tb_package_traffic_alert_rule.id IS '主键';
COMMENT ON COLUMN tb_package_traffic_alert_rule.package_id IS '套餐商品ID唯一约束保证每个商品至多一条当前规则';
COMMENT ON COLUMN tb_package_traffic_alert_rule.threshold_percent IS '真流量预警阈值百分比,取值 1100允许两位小数';
COMMENT ON COLUMN tb_package_traffic_alert_rule.enabled IS '状态 0-禁用 1-启用;停用后扫描不再创建新预警,既有预警快照不变';
COMMENT ON COLUMN tb_package_traffic_alert_rule.remark IS '备注,最多 500 字符';
COMMENT ON COLUMN tb_package_traffic_alert_rule.creator IS '创建人账号ID';
COMMENT ON COLUMN tb_package_traffic_alert_rule.updater IS '最近更新人账号ID';
COMMENT ON COLUMN tb_package_traffic_alert_rule.created_at IS '创建时间';
COMMENT ON COLUMN tb_package_traffic_alert_rule.updated_at IS '最近更新时间,阈值修改与启停均必须刷新';
CREATE TABLE tb_package_traffic_alert (
id BIGSERIAL PRIMARY KEY,
package_usage_id BIGINT NOT NULL,
package_id BIGINT NOT NULL,
rule_id BIGINT NOT NULL,
asset_type VARCHAR(16) NOT NULL,
asset_id BIGINT NOT NULL,
asset_identifier_snapshot VARCHAR(100) NOT NULL DEFAULT '',
card_identifier_snapshot VARCHAR(100) NOT NULL DEFAULT '',
counterpart_identifier_snapshot VARCHAR(100) NOT NULL DEFAULT '',
device_type_snapshot VARCHAR(50) NOT NULL DEFAULT '',
device_model_snapshot VARCHAR(100) NOT NULL DEFAULT '',
package_name_snapshot VARCHAR(255) NOT NULL DEFAULT '',
used_mb_snapshot BIGINT NOT NULL DEFAULT 0,
limit_mb_snapshot BIGINT NOT NULL,
usage_percent_snapshot NUMERIC(9, 2) NOT NULL DEFAULT 0,
threshold_percent_snapshot NUMERIC(5, 2) NOT NULL,
expires_at_snapshot TIMESTAMPTZ,
triggered_at TIMESTAMPTZ NOT NULL,
shop_id_snapshot BIGINT NOT NULL DEFAULT 0,
shop_name_snapshot VARCHAR(100) NOT NULL DEFAULT '',
business_owner_account_id_snapshot BIGINT,
business_owner_name_snapshot VARCHAR(64) NOT NULL DEFAULT '',
notification_event_id VARCHAR(64) NOT NULL DEFAULT '',
creator BIGINT NOT NULL DEFAULT 0,
updater BIGINT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uq_package_traffic_alert_usage_threshold UNIQUE (package_usage_id, threshold_percent_snapshot),
CONSTRAINT ck_package_traffic_alert_asset_type CHECK (asset_type IN ('iot_card', 'device')),
CONSTRAINT ck_package_traffic_alert_threshold CHECK (threshold_percent_snapshot >= 1 AND threshold_percent_snapshot <= 100),
CONSTRAINT ck_package_traffic_alert_limit CHECK (limit_mb_snapshot > 0),
CONSTRAINT ck_package_traffic_alert_used CHECK (used_mb_snapshot >= 0),
CONSTRAINT ck_package_traffic_alert_percent CHECK (usage_percent_snapshot >= 0)
);
COMMENT ON TABLE tb_package_traffic_alert IS '套餐真流量达量预警事实,触发时冻结阈值、汇总用量与资产归属快照';
COMMENT ON COLUMN tb_package_traffic_alert.id IS '主键';
COMMENT ON COLUMN tb_package_traffic_alert.package_usage_id IS '主套餐使用记录IDmaster_usage_id 为空且按优先级/生效时间/编号取第一条),预警锚点';
COMMENT ON COLUMN tb_package_traffic_alert.package_id IS '阈值来源的套餐商品ID快照';
COMMENT ON COLUMN tb_package_traffic_alert.rule_id IS '触发时的预警规则ID快照';
COMMENT ON COLUMN tb_package_traffic_alert.asset_type IS '资产类型 iot_card-物联网卡 device-设备';
COMMENT ON COLUMN tb_package_traffic_alert.asset_id IS '资产ID取自使用记录的绑定资产是权威归属';
COMMENT ON COLUMN tb_package_traffic_alert.asset_identifier_snapshot IS '资产标识快照:卡取 ICCID设备取虚拟号→IMEI→SN 的稳定优先级';
COMMENT ON COLUMN tb_package_traffic_alert.card_identifier_snapshot IS '卡标识快照:卡资产为自身 ICCID设备资产为触发时当前绑定卡 ICCID';
COMMENT ON COLUMN tb_package_traffic_alert.counterpart_identifier_snapshot IS '对应标识符快照:卡资产为触发时当前绑定设备标识,设备资产为触发时当前绑定卡 ICCID';
COMMENT ON COLUMN tb_package_traffic_alert.device_type_snapshot IS '设备类型快照:设备资产取自身,卡资产取触发时当前绑定设备';
COMMENT ON COLUMN tb_package_traffic_alert.device_model_snapshot IS '设备型号快照:设备资产取自身,卡资产取触发时当前绑定设备';
COMMENT ON COLUMN tb_package_traffic_alert.package_name_snapshot IS '套餐名称快照,优先使用记录快照,缺失时回落商品名称';
COMMENT ON COLUMN tb_package_traffic_alert.used_mb_snapshot IS '触发时该资产全部当前有效套餐的真已用量汇总(MB)';
COMMENT ON COLUMN tb_package_traffic_alert.limit_mb_snapshot IS '触发时该资产全部当前有效套餐的真总量快照汇总(MB)';
COMMENT ON COLUMN tb_package_traffic_alert.usage_percent_snapshot IS '触发时汇总比例快照,单位百分比,保留两位小数';
COMMENT ON COLUMN tb_package_traffic_alert.threshold_percent_snapshot IS '触发阈值快照,单位百分比;与使用记录组成唯一键';
COMMENT ON COLUMN tb_package_traffic_alert.expires_at_snapshot IS '主套餐使用记录到期时间快照;为空时导出到期时间与剩余天数为空';
COMMENT ON COLUMN tb_package_traffic_alert.triggered_at IS '触发时间,列表与导出按该列筛选';
COMMENT ON COLUMN tb_package_traffic_alert.shop_id_snapshot IS '触发时资产所属店铺ID快照';
COMMENT ON COLUMN tb_package_traffic_alert.shop_name_snapshot IS '触发时店铺名称快照';
COMMENT ON COLUMN tb_package_traffic_alert.business_owner_account_id_snapshot IS '触发时店铺业务员账号ID快照无有效业务员为空';
COMMENT ON COLUMN tb_package_traffic_alert.business_owner_name_snapshot IS '触发时业务员账号名快照;无有效业务员为空字符串';
COMMENT ON COLUMN tb_package_traffic_alert.notification_event_id IS '可靠通知事件ID仅在写入通知事件时填充无有效业务员时为空';
COMMENT ON COLUMN tb_package_traffic_alert.creator IS '创建人账号ID扫描任务写入 0';
COMMENT ON COLUMN tb_package_traffic_alert.updater IS '最近更新人账号ID预警事实创建后不再改写';
COMMENT ON COLUMN tb_package_traffic_alert.created_at IS '创建时间';
COMMENT ON COLUMN tb_package_traffic_alert.updated_at IS '最近更新时间';
-- 列表默认按触发时间倒序;店铺、套餐与资产筛选各自走独立索引。
CREATE INDEX idx_package_traffic_alert_triggered ON tb_package_traffic_alert (triggered_at DESC, id DESC);
CREATE INDEX idx_package_traffic_alert_shop ON tb_package_traffic_alert (shop_id_snapshot, triggered_at DESC, id DESC);
CREATE INDEX idx_package_traffic_alert_package ON tb_package_traffic_alert (package_id, triggered_at DESC, id DESC);
CREATE INDEX idx_package_traffic_alert_asset ON tb_package_traffic_alert (asset_type, asset_id);
CREATE INDEX idx_package_traffic_alert_event ON tb_package_traffic_alert (notification_event_id);
-- 扫描只看当前有效套餐集合status IN (1,2)、未退款、未软删。
-- 该索引为非唯一部分索引,只服务只读聚合,不与任何 OnConflict 组合,因此不涉及部分唯一索引谓词问题。
CREATE INDEX idx_package_usage_alert_scope
ON tb_package_usage (iot_card_id, device_id, master_usage_id)
WHERE deleted_at IS NULL AND refund_id IS NULL AND status IN (1, 2);
COMMENT ON INDEX idx_package_usage_alert_scope IS '套餐真流量达量扫描的有效套餐集合部分索引';