All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 13m13s
新增 000226 迁移:规则表 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。
126 lines
11 KiB
SQL
126 lines
11 KiB
SQL
-- 套餐真流量达量预警:规则表与预警快照表,以及扫描用的有效套餐部分索引。
|
||
-- 背景:运营需要按「同一资产全部当前有效套餐的真流量汇总」判断达量,并向触发时资产所属店铺的
|
||
-- 有效平台业务员投递站内通知。既有事实里没有任何承载「套餐商品真流量预警规则」的表,
|
||
-- 也没有可冻结触发阈值、汇总用量、归属与资产展示快照的预警表,因此本迁移新增两张表;
|
||
-- 既有 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 '真流量预警阈值百分比,取值 1~100,允许两位小数';
|
||
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 '主套餐使用记录ID(master_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 '套餐真流量达量扫描的有效套餐集合部分索引';
|