-- 卡轮询优先队列补齐事实字段与两个新终态。 -- 背景:111.md §23.4/§23.6/§23.7/§23.9/§23.10 要求优先项可追溯资产、设备号、店铺与代理归属、 -- 尝试上限、执行起止时间、下一次执行时间、出队时间、集成交互日志标识与有效期起算时间,并要求 -- 支持「有效期到期出队」与「人工关闭出队」两个出口。既有 tb_polling_priority_item 只有执行结果、 -- 失败原因与尝试次数,读侧无法解释「什么时候执行、下一次什么时候执行、为什么出队」, -- 也没有承载已关闭与已过期两个终态的取值空间。 -- -- 设计选择: -- 1. 只加列与重建受影响的 CHECK,不改活动项唯一键:活动集合仍是 pending/processing, -- 部分唯一索引谓词 status IN ('pending','processing') 保持原样,两个新终态不占键位, -- 同一卡同一任务类型出队后可再次入队或人工重触发。 -- 2. 资产列(asset_type/asset_id)按既有设备—卡绑定回填:卡已绑定设备时资产为该设备, -- 独立卡回填为卡自身;设备号快照取绑定设备的虚拟号,未绑定设备时为空字符串。 -- 两列允许空值/空串(CHECK 兼容旧代码回滚期不写这两列的插入),新代码入队必写。 -- 3. priority_effective_from 是有效期起算时间:既有活动项在 up 中回填为迁移时刻, -- 使上线后首轮到期扫描不会把既有未完成项批量判为已过期;终态行留空(已出队,不再计算有效期)。 -- 新增行取入队时间。有效期长度与尝试上限均为代码常量,本迁移不新增可维护配置项。 -- 4. attempt_limit 默认 3 与既有代码常量一致,既有行随默认值获得尝试上限。 -- 5. next_run_at 可空:既有行没有下次计划执行时间,读侧 MUST NOT 据此判定已过期。 -- 为它建部分索引以支撑到期扫描与读侧筛选。 -- 6. 状态 CHECK 重建并新增 closed-已关闭 expired-已过期 两个终态;终态与结果约束同步放宽, -- 使两个新终态可以保留既有执行结果与失败原因而不被约束拒绝。 -- 7. 不使用数据库外键;新增列由应用层显式写入。 ALTER TABLE tb_polling_priority_item ADD COLUMN asset_type VARCHAR(20) NOT NULL DEFAULT '', ADD COLUMN asset_id BIGINT, ADD COLUMN device_no_snapshot VARCHAR(100) NOT NULL DEFAULT '', ADD COLUMN agent_shop_id_snapshot BIGINT, ADD COLUMN attempt_limit INT NOT NULL DEFAULT 3, ADD COLUMN started_at TIMESTAMPTZ, ADD COLUMN finished_at TIMESTAMPTZ, ADD COLUMN next_run_at TIMESTAMPTZ, ADD COLUMN dequeued_at TIMESTAMPTZ, ADD COLUMN integration_log_id VARCHAR(64) NOT NULL DEFAULT '', ADD COLUMN priority_effective_from TIMESTAMPTZ; -- 资产列回填:优先取该卡当前有效的设备绑定(同一卡存在多条绑定时取 is_current 优先、其次最新一条)。 UPDATE tb_polling_priority_item AS i SET asset_type = 'device', asset_id = b.device_id, device_no_snapshot = COALESCE(d.virtual_no, '') FROM ( SELECT DISTINCT ON (iot_card_id) iot_card_id, device_id FROM tb_device_sim_binding WHERE bind_status = 1 AND deleted_at IS NULL ORDER BY iot_card_id, is_current DESC, id DESC ) AS b LEFT JOIN tb_device AS d ON d.id = b.device_id AND d.deleted_at IS NULL WHERE b.iot_card_id = i.card_id; -- 未被上一步覆盖的行即独立卡:资产为卡自身。 UPDATE tb_polling_priority_item SET asset_type = 'iot_card', asset_id = card_id WHERE asset_id IS NULL; -- 代理归属回填:卡所属店铺的直接上级代理店铺(一级代理与平台卡为空)。 UPDATE tb_polling_priority_item AS i SET agent_shop_id_snapshot = s.parent_id FROM tb_iot_card AS c JOIN tb_shop AS s ON s.id = c.shop_id AND s.deleted_at IS NULL WHERE c.id = i.card_id AND c.deleted_at IS NULL AND s.parent_id IS NOT NULL; -- 既有活动项的有效期自迁移时刻起算,避免上线后被首轮到期扫描批量过期。 UPDATE tb_polling_priority_item SET priority_effective_from = NOW() WHERE deleted_at IS NULL AND status IN ('pending', 'processing'); COMMENT ON COLUMN tb_polling_priority_item.asset_type IS '资产类型 iot_card-物联网卡 device-设备,与 pkg/constants.AssetTypeIotCard/AssetTypeDevice 一致;卡绑定设备时资产为设备,独立卡为卡自身'; COMMENT ON COLUMN tb_polling_priority_item.asset_id IS '资产ID,与 asset_type 共同构成资产身份;入队时按设备—卡绑定解析'; COMMENT ON COLUMN tb_polling_priority_item.device_no_snapshot IS '设备号快照(绑定设备的虚拟号),独立卡为空字符串'; COMMENT ON COLUMN tb_polling_priority_item.agent_shop_id_snapshot IS '代理归属快照-卡所属店铺的直接上级代理店铺ID,一级代理与平台卡为空'; COMMENT ON COLUMN tb_polling_priority_item.attempt_limit IS '尝试上限,入队时取代码常量(3),既有行随默认值获得上限;不新增可维护配置项'; COMMENT ON COLUMN tb_polling_priority_item.started_at IS '最近一次执行的开始时间,只在真正发起执行后写入'; COMMENT ON COLUMN tb_polling_priority_item.finished_at IS '最近一次执行的结束时间,与开始时间共同表达执行耗时'; COMMENT ON COLUMN tb_polling_priority_item.next_run_at IS '下一次计划执行时间,可空;为空表示未排期,读侧 MUST NOT 据此判定已过期'; COMMENT ON COLUMN tb_polling_priority_item.dequeued_at IS '出队时间,终态(已完成、失败出队、已关闭、已过期)写入'; COMMENT ON COLUMN tb_polling_priority_item.integration_log_id IS '最近一次上游交互的集成交互日志标识,不复制凭证或响应原文'; COMMENT ON COLUMN tb_polling_priority_item.priority_effective_from IS '有效期起算时间,新增行取入队时间;既有活动项在迁移时刻回填,终态行留空'; -- 取值域约束:允许空串/空值以兼容旧代码回滚期的插入(旧版本不写资产列); -- 一旦写入则必须是合法资产类型与正数资产ID。 ALTER TABLE tb_polling_priority_item ADD CONSTRAINT ck_polling_priority_item_asset_type CHECK (asset_type IN ('', 'iot_card', 'device')), ADD CONSTRAINT ck_polling_priority_item_asset CHECK (asset_id IS NULL OR asset_id > 0), ADD CONSTRAINT ck_polling_priority_item_attempt_limit CHECK (attempt_limit > 0); -- 状态约束重建:新增 closed-已关闭 与 expired-已过期 两个终态。 ALTER TABLE tb_polling_priority_item DROP CONSTRAINT ck_polling_priority_item_status; ALTER TABLE tb_polling_priority_item ADD CONSTRAINT ck_polling_priority_item_status CHECK ( status IN ('pending', 'processing', 'completed', 'failed', 'closed', 'expired') ); -- 终态与结果的约束同步放宽:两个新终态保留既有执行结果与失败原因,不参与「必须带结果」的强约束。 ALTER TABLE tb_polling_priority_item DROP CONSTRAINT ck_polling_priority_item_terminal_result; ALTER TABLE tb_polling_priority_item ADD CONSTRAINT ck_polling_priority_item_terminal_result CHECK ( (status = 'completed' AND result = 'success') OR (status = 'failed' AND result = 'failed') OR status IN ('pending', 'processing', 'closed', 'expired') ); -- 到期扫描与读侧筛选按下一次计划执行时间取活动项。 CREATE INDEX idx_polling_priority_item_next_run ON tb_polling_priority_item (next_run_at) WHERE deleted_at IS NULL AND status IN ('pending', 'processing') AND next_run_at IS NOT NULL; COMMENT ON INDEX idx_polling_priority_item_next_run IS '优先队列活动项的下一次计划执行时间部分索引,服务到期扫描与读侧筛选';