All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 13m29s
115 lines
10 KiB
SQL
115 lines
10 KiB
SQL
-- 运营商通道流量阈值停机: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/unknown/failed 都必须继续用只读状态查询收敛)。
|
||
CREATE INDEX idx_carrier_traffic_threshold_lock_unresolved ON tb_carrier_traffic_threshold_lock (stop_status, resume_status)
|
||
WHERE deleted_at IS NULL AND (
|
||
stop_status IN ('submitted', 'unknown', 'failed') OR resume_status IN ('submitted', 'unknown', 'failed')
|
||
);
|
||
|
||
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_unresolved IS '通道阈值恢复扫描的未决子任务部分索引(含结果未知与失败,谓词与 domain.UnresolvedTaskStatuses 一致)';
|
||
COMMENT ON INDEX uq_carrier_traffic_threshold_lock_key IS '通道阈值周期锁唯一键(软删感知):一个卡在一个运营商计费周期内至多一条,冲突即该周期已处理';
|