feat(通道流量阈值): AUG26-011 运营商通道流量阈值达量停机与周期复机
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 12m59s

This commit is contained in:
2026-09-16 15:54:49 +08:00
parent 41722760b1
commit 59b3df868a
36 changed files with 2431 additions and 93 deletions

View File

@@ -0,0 +1,36 @@
-- 回滚运营商通道流量阈值停机:删除周期阈值停机锁表与 tb_carrier 的阈值三列,与 up 严格成对。
-- 先删锁表索引与锁表,再删除 tb_carrier 的三个阈值列,顺序与 up 的创建顺序严格倒序。
--
-- 不可逆说明ENG-MIG-001 例外条件):本迁移的 down 会删除 tb_carrier_traffic_threshold_lock 与
-- tb_carrier 的 traffic_threshold_enabled/traffic_threshold_value/traffic_threshold_unit 三列。
-- 已触发的阈值停机事实(锁行)与运营商阈值配置都无法由数据库自身重建,只能依据
-- tb_audit_event 中 carrier_create/carrier_update 的前后值快照与 Integration Log 人工复核,
-- 因此 down 只在上述事实已无留存需求时执行。
--
-- 守卫一:锁表存在任何未软删行时直接阻断回滚,避免静默丢失阈值停机/复机事实与恢复扫描状态。
-- 守卫二:存在任一已配置阈值(启用或已填数值)的运营商时直接阻断回滚,避免静默丢失阈值配置。
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM tb_carrier_traffic_threshold_lock WHERE deleted_at IS NULL LIMIT 1) THEN
RAISE EXCEPTION '存在通道流量阈值周期锁事实,拒绝回滚以避免丢失停机/复机事实与恢复扫描状态';
END IF;
END $$;
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM tb_carrier WHERE traffic_threshold_enabled = 1 OR traffic_threshold_value IS NOT NULL LIMIT 1) THEN
RAISE EXCEPTION '存在已配置通道流量阈值的运营商,拒绝回滚以避免丢失阈值配置';
END IF;
END $$;
DROP INDEX IF EXISTS idx_carrier_traffic_threshold_lock_submitted;
DROP INDEX IF EXISTS idx_carrier_traffic_threshold_lock_card;
DROP INDEX IF EXISTS idx_carrier_traffic_threshold_lock_status_period;
DROP TABLE IF EXISTS tb_carrier_traffic_threshold_lock;
ALTER TABLE tb_carrier
DROP COLUMN IF EXISTS traffic_threshold_enabled,
DROP COLUMN IF EXISTS traffic_threshold_value,
DROP COLUMN IF EXISTS traffic_threshold_unit;

View File

@@ -0,0 +1,112 @@
-- 运营商通道流量阈值停机tb_carrier 增加阈值配置列,并新增「按周期锁」表承载阈值停机/复机事实。
-- 背景:运营需要按运营商配置的计费周期流量阈值自动停复机。既有 tb_carrier 没有任何承载
-- 「通道流量阈值」的列,也没有任何表承载「某卡在某计费周期内是否已触发阈值停机」的事实,
-- 因此本迁移扩展 tb_carrier 三个列并新增 tb_carrier_traffic_threshold_lock 表。
--
-- 设计选择:
-- 1. 阈值三列全部可空/默认禁用enabled 默认 0未启用value 与 unit 仅在启用时有意义,
-- 既有运营商行零变更unit 仅允许 MB/GB由应用层换算为 MB 后判定1GB=1024MB
-- 2. 锁表以 (carrier_id, card_id, period_start) 为唯一键:一个卡在一个计费周期内至多一条锁,
-- 重复达量观测由唯一冲突视为「该周期已处理」,不重复停机、不重放事件。
-- 唯一键为部分唯一索引WHERE deleted_at IS NULL避免软删行占用键位。
-- 注意:该唯一键不与任何 GORM OnConflict 组合(应用层用显式插入 + 23505 识别),
-- 因此不涉及部分唯一索引谓词未声明导致 OnConflict 失败的问题KNOWN-ISSUE-001
-- 3. period_start 为 timestamptz存「该卡所属运营商计费周期的起点」UTC+8 口径的零点),
-- 由应用层按 data_reset_day 计算后写入;恢复扫描与周期处理据此判断是否跨期。
-- 4. status 表达整行生命周期locked 进行中 / unlocked 已跨期解锁stop_status 与
-- resume_status 分别表达停机与复机两个子任务pending 待提交 / submitted 已提交待确认 /
-- confirmed 已确认 / failed 已失败 / unknown 结果未知anomaly_flag 标记需人工核对的异常,
-- failure_reason 只写可安全对外展示的原因,不写内部细节。
-- 5. *_submitted_at 记录子任务进入 submitted 的时刻,恢复扫描据此判断查询窗口是否超期。
-- 6. *_integration_id 记录对应停/复机 Gateway 调用的 Integration Log 标识,便于人工核对。
-- 7. 不使用数据库外键carrier_id 与 card_id 由应用层显式校验。
-- 8. creator/updater 记录维护账号,系统写入为 0。
ALTER TABLE tb_carrier
ADD COLUMN traffic_threshold_enabled SMALLINT NOT NULL DEFAULT 0,
ADD COLUMN traffic_threshold_value NUMERIC(18,2),
ADD COLUMN traffic_threshold_unit VARCHAR(8) NOT NULL DEFAULT '';
COMMENT ON COLUMN tb_carrier.traffic_threshold_enabled IS '通道流量阈值是否启用 0-未启用 1-已启用;未启用时不参与达量停机判定';
COMMENT ON COLUMN tb_carrier.traffic_threshold_value IS '通道流量阈值数值(正数),单位由 traffic_threshold_unit 决定NULL-未配置,启用时必填';
COMMENT ON COLUMN tb_carrier.traffic_threshold_unit IS '通道流量阈值单位 MB-兆字节 GB-吉字节,空字符串-未配置,启用时必填';
-- 阈值配置在数据库层就不允许出现「启用但无数值/单位」或「非正数阈值/未知单位」的半配置状态:
-- 半配置会被达量判定解释为阈值 0 而立即停机,属于必须由 Schema 兜住的风险。
-- 未启用时允许保留数值与单位(停用只停止新锁创建,不丢配置);未配置表示为 value IS NULL + unit = ''。
-- 启停列必须是严格的 0/1判定端按 enabled = 1 识别启用,其他取值会被静默视为未启用,
-- 因此非 0/1 的半残取值必须在 Schema 与业务边界一起拒绝。
ALTER TABLE tb_carrier
ADD CONSTRAINT ck_carrier_traffic_threshold_config CHECK (
traffic_threshold_enabled IN (0, 1)
AND (traffic_threshold_value IS NULL OR traffic_threshold_value > 0)
AND traffic_threshold_unit IN ('', 'MB', 'GB')
AND (traffic_threshold_enabled = 0
OR (traffic_threshold_value IS NOT NULL AND traffic_threshold_unit IN ('MB', 'GB')))
);
CREATE TABLE tb_carrier_traffic_threshold_lock (
id BIGSERIAL PRIMARY KEY,
carrier_id BIGINT NOT NULL,
card_id BIGINT NOT NULL,
period_start TIMESTAMPTZ NOT NULL,
status VARCHAR(16) NOT NULL DEFAULT 'locked',
stop_status VARCHAR(16) NOT NULL DEFAULT 'pending',
resume_status VARCHAR(16) NOT NULL DEFAULT 'pending',
stop_submitted_at TIMESTAMPTZ,
resume_submitted_at TIMESTAMPTZ,
stop_integration_id VARCHAR(64) NOT NULL DEFAULT '',
resume_integration_id VARCHAR(64) NOT NULL DEFAULT '',
anomaly_flag SMALLINT NOT NULL DEFAULT 0,
failure_reason 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(),
deleted_at TIMESTAMPTZ,
CONSTRAINT ck_carrier_traffic_threshold_lock_status CHECK (status IN ('locked', 'unlocked')),
CONSTRAINT ck_carrier_traffic_threshold_lock_stop_status CHECK (stop_status IN ('pending', 'submitted', 'confirmed', 'failed', 'unknown')),
CONSTRAINT ck_carrier_traffic_threshold_lock_resume_status CHECK (resume_status IN ('pending', 'submitted', 'confirmed', 'failed', 'unknown')),
CONSTRAINT ck_carrier_traffic_threshold_lock_anomaly CHECK (anomaly_flag IN (0, 1))
);
-- 唯一键必须是「部分唯一索引」CREATE TABLE 的表约束不支持 WHERE 谓词(实测 pq: syntax error at or near "WHERE"
-- 而软删感知要求谓词 deleted_at IS NULL 使软删行不占用键位,因此独立建索引。
-- 该索引不与任何 GORM OnConflict 组合(部分唯一索引谓词未声明时 OnConflict 无法命中,见 KNOWN-ISSUE-001
-- 应用层以显式插入 + 23505 识别表达「该周期已处理」。
CREATE UNIQUE INDEX uq_carrier_traffic_threshold_lock_key
ON tb_carrier_traffic_threshold_lock (carrier_id, card_id, period_start)
WHERE deleted_at IS NULL;
COMMENT ON TABLE tb_carrier_traffic_threshold_lock IS '运营商通道流量阈值按周期锁,一个卡一个计费周期至多一条,承载阈值停机/复机事实与恢复扫描状态';
COMMENT ON COLUMN tb_carrier_traffic_threshold_lock.id IS '主键';
COMMENT ON COLUMN tb_carrier_traffic_threshold_lock.carrier_id IS '触发阈值时的运营商ID周期起点与恢复扫描均按该运营商的 data_reset_day 计算';
COMMENT ON COLUMN tb_carrier_traffic_threshold_lock.card_id IS '触发阈值的物联网卡ID';
COMMENT ON COLUMN tb_carrier_traffic_threshold_lock.period_start IS '触发时的计费周期起点UTC+8 零点口径),与唯一键共同表达「哪个周期」';
COMMENT ON COLUMN tb_carrier_traffic_threshold_lock.status IS '整行生命周期 locked-进行中 unlocked-已跨期解锁';
COMMENT ON COLUMN tb_carrier_traffic_threshold_lock.stop_status IS '停机子任务 pending-待提交 submitted-已提交待确认 confirmed-已确认 failed-已失败 unknown-结果未知';
COMMENT ON COLUMN tb_carrier_traffic_threshold_lock.resume_status IS '复机子任务 pending-待提交 submitted-已提交待确认 confirmed-已确认 failed-已失败 unknown-结果未知';
COMMENT ON COLUMN tb_carrier_traffic_threshold_lock.stop_submitted_at IS '停机进入 submitted 的时刻,恢复扫描据此判断查询窗口';
COMMENT ON COLUMN tb_carrier_traffic_threshold_lock.resume_submitted_at IS '复机进入 submitted 的时刻,恢复扫描据此判断查询窗口';
COMMENT ON COLUMN tb_carrier_traffic_threshold_lock.stop_integration_id IS '停机 Gateway 调用的 Integration Log 标识,便于人工核对';
COMMENT ON COLUMN tb_carrier_traffic_threshold_lock.resume_integration_id IS '复机 Gateway 调用的 Integration Log 标识,便于人工核对';
COMMENT ON COLUMN tb_carrier_traffic_threshold_lock.anomaly_flag IS '异常标记 0-正常 1-需人工核对(查询窗口超期或失败无法自动确认)';
COMMENT ON COLUMN tb_carrier_traffic_threshold_lock.failure_reason IS '可安全对外展示的原因,不写内部细节';
COMMENT ON COLUMN tb_carrier_traffic_threshold_lock.creator IS '创建人账号ID系统写入为 0';
COMMENT ON COLUMN tb_carrier_traffic_threshold_lock.updater IS '最近更新人账号ID系统写入为 0';
COMMENT ON COLUMN tb_carrier_traffic_threshold_lock.created_at IS '创建时间';
COMMENT ON COLUMN tb_carrier_traffic_threshold_lock.updated_at IS '最近更新时间';
COMMENT ON COLUMN tb_carrier_traffic_threshold_lock.deleted_at IS '软删除时间,正常流程不删除,仅数据清理使用';
-- 周期处理扫描进行中锁:按 (status, period_start) 取「当前周期之前」的 locked 行。
CREATE INDEX idx_carrier_traffic_threshold_lock_status_period ON tb_carrier_traffic_threshold_lock (status, period_start);
-- 四入口拒绝与周期处理按卡查当前周期锁。
CREATE INDEX idx_carrier_traffic_threshold_lock_card ON tb_carrier_traffic_threshold_lock (card_id, period_start);
-- 恢复扫描只看 submitted 子任务。
CREATE INDEX idx_carrier_traffic_threshold_lock_submitted ON tb_carrier_traffic_threshold_lock (stop_status, resume_status)
WHERE deleted_at IS NULL AND (stop_status = 'submitted' OR resume_status = 'submitted');
COMMENT ON INDEX idx_carrier_traffic_threshold_lock_status_period IS '通道阈值周期处理扫描的进行中锁索引';
COMMENT ON INDEX idx_carrier_traffic_threshold_lock_card IS '通道阈值按卡查当前周期锁索引';
COMMENT ON INDEX idx_carrier_traffic_threshold_lock_submitted IS '通道阈值恢复扫描的已提交子任务部分索引';
COMMENT ON INDEX uq_carrier_traffic_threshold_lock_key IS '通道阈值周期锁唯一键(软删感知):一个卡在一个运营商计费周期内至多一条,冲突即该周期已处理';