-- 运营商通道阈值命中事实留痕。 -- 背景:111.md 要求阈值命中可解释、可查询:停机后运维必须能看到「命中时该卡累计流量多少、 -- 判定用的阈值是多少(含改阈值前的历史值)、属于哪个计费周期、什么时候判定、由什么触发」, -- 并能在周期恢复时看到解锁时间与复机结果。既有 tb_carrier_traffic_threshold_lock 只有 -- 锁生命周期与停复机子任务状态,没有任何数字证据,改阈值后历史命中无法解释。 -- -- 设计选择: -- 1. 加列到既有锁表而不新建命中事实表:锁表本身即「一次命中一条」(一个卡一个计费周期至多一条), -- 新建表会与锁表 1:1 冗余并需要额外一致性约束。 -- 2. hit_* 三列冻结命中时值:hit_traffic_mb 取与停机判定同一来源的网关累计读数换算值(MB, -- 不使用本地用量或套餐真流量);hit_threshold_value/hit_threshold_unit 取该次判定实际生效的 -- 通道配置值,因此之后修改通道阈值不会改写历史命中。 -- 3. judged_at 是判定时间:既有行以创建时间回填(历史读数与阈值不可重建),回填后置为非空。 -- hit_traffic_mb 与 hit_threshold_value 允许为空,读侧按「无」展示而不阻断查询。 -- 4. trigger_source 记录触发本次判定的流量观测来源(与 pkg/constants.CardObservationSource* -- 一致),使自动判定与重放可区分;允许空串以兼容回滚期旧代码插入。 -- 5. unlocked_at/resume_result 记录周期恢复结果,MUST NOT 覆盖上面的停机命中事实: -- resumed-已发起复机 skipped-仅解锁未复机 manual-周期归属不可判定转人工;跳过原因沿用既有失败原因列。 -- 6. 新增按通道与按卡的判定时间索引,服务命中记录查询的筛选与排序。 -- 7. 本迁移不改动既有达量判定、停机调用、持锁拒绝复机与新周期解锁语义。 ALTER TABLE tb_carrier_traffic_threshold_lock ADD COLUMN hit_traffic_mb NUMERIC(18,2), ADD COLUMN hit_threshold_value NUMERIC(18,2), ADD COLUMN hit_threshold_unit VARCHAR(8) NOT NULL DEFAULT '', ADD COLUMN judged_at TIMESTAMPTZ, ADD COLUMN trigger_source VARCHAR(20) NOT NULL DEFAULT '', ADD COLUMN unlocked_at TIMESTAMPTZ, ADD COLUMN resume_result VARCHAR(30) NOT NULL DEFAULT ''; -- 历史行判定时间取创建时间;命中数值与阈值留空(历史读数不可重建,读侧按「无」展示)。 UPDATE tb_carrier_traffic_threshold_lock SET judged_at = created_at WHERE judged_at IS NULL; ALTER TABLE tb_carrier_traffic_threshold_lock ALTER COLUMN judged_at SET NOT NULL; -- judged_at 保留默认值 NOW():本次硬规则要求「新列可空或带默认值」,而 judged_at 需要非空 -- (判定时间是命中记录的查询与排序依据)。若不给默认值,回滚到上一版本二进制时,旧代码写入 -- 停机锁不带该列会直接违反 NOT NULL,使达量停机事实无法落库。默认值让旧代码插入的命中事实 -- 以插入时刻作为判定时间,与「既有行以创建时间回填」的语义自洽。 ALTER TABLE tb_carrier_traffic_threshold_lock ALTER COLUMN judged_at SET DEFAULT NOW(); ALTER TABLE tb_carrier_traffic_threshold_lock ADD CONSTRAINT ck_carrier_traffic_threshold_lock_hit_traffic CHECK (hit_traffic_mb IS NULL OR hit_traffic_mb >= 0), ADD CONSTRAINT ck_carrier_traffic_threshold_lock_hit_threshold CHECK (hit_threshold_value IS NULL OR hit_threshold_value > 0), ADD CONSTRAINT ck_carrier_traffic_threshold_lock_hit_unit CHECK (hit_threshold_unit IN ('', 'MB', 'GB')), ADD CONSTRAINT ck_carrier_traffic_threshold_lock_trigger_source CHECK ( trigger_source IN ('', 'polling', 'manual_sync', 'manual_override', 'carrier_callback', 'business_event') ), ADD CONSTRAINT ck_carrier_traffic_threshold_lock_resume_result CHECK ( resume_result IN ('', 'resumed', 'skipped', 'manual') ); COMMENT ON COLUMN tb_carrier_traffic_threshold_lock.hit_traffic_mb IS '命中时该卡当前计费周期的累计流量读数(MB),取与停机判定同一来源的网关累计读数换算值,不使用本地用量或套餐真流量;历史行为空表示读数不可重建'; COMMENT ON COLUMN tb_carrier_traffic_threshold_lock.hit_threshold_value IS '本次判定实际生效的通道阈值数值,冻结命中时值,改阈值后历史命中不被改写;历史行为空'; COMMENT ON COLUMN tb_carrier_traffic_threshold_lock.hit_threshold_unit IS '本次判定阈值的单位 MB-兆字节 GB-吉字节,空字符串-历史行未记录'; COMMENT ON COLUMN tb_carrier_traffic_threshold_lock.judged_at IS '判定时间,既有行以创建时间回填;命中记录查询与排序以此为业务时间。列默认值为 NOW(),使回滚期不写该列的旧代码插入的命中事实以插入时刻作为判定时间'; COMMENT ON COLUMN tb_carrier_traffic_threshold_lock.trigger_source IS '触发本次判定的流量观测来源 polling-周期轮询 manual_sync-手动同步 manual_override-人工纠偏 carrier_callback-运营商回调 business_event-业务成功后的观测;空字符串-历史行未记录'; COMMENT ON COLUMN tb_carrier_traffic_threshold_lock.unlocked_at IS '周期恢复时的解锁时间,只在跨期解锁成功时写入;不覆盖停机命中事实'; COMMENT ON COLUMN tb_carrier_traffic_threshold_lock.resume_result IS '周期恢复的复机结果 resumed-已发起复机 skipped-仅解锁未复机 manual-周期归属不可判定转人工;空字符串-未处理或历史行,跳过原因沿用失败原因列'; -- 命中记录查询按判定时间倒序:分别按运营商通道与按卡提供索引,避免查询走顺序扫描。 CREATE INDEX idx_carrier_traffic_threshold_lock_carrier_judged ON tb_carrier_traffic_threshold_lock (carrier_id, judged_at DESC, id DESC) WHERE deleted_at IS NULL; CREATE INDEX idx_carrier_traffic_threshold_lock_card_judged ON tb_carrier_traffic_threshold_lock (card_id, judged_at DESC, id DESC) WHERE deleted_at IS NULL; COMMENT ON INDEX idx_carrier_traffic_threshold_lock_carrier_judged IS '通道阈值命中记录按通道与判定时间分页索引'; COMMENT ON INDEX idx_carrier_traffic_threshold_lock_card_judged IS '通道阈值命中记录按卡与判定时间分页索引';