-- H5 运营弹窗配置与弹窗投放快照。 -- 背景:H5 需要在客户实际访问时投放风险换卡或运营弹窗,并把投放内容冻结为个人站内通知。 -- 既有事实只有个人站内通知(tb_notification)与换货单(tb_exchange_order), -- 没有任何可承载「页面、店铺/设备类型/卡类型范围、优先级、频率、受控动作、启停与有效期」的配置表, -- 因此本迁移新增 tb_h5_popup_configuration;同时为 tb_notification 增加一个可空 JSONB 快照列, -- 用于冻结弹窗投放时的配置 ID、配置版本、资产与受控动作。既有通知列、类别 CHECK 与保留清理均不改动。 -- -- 设计选择: -- 1. version 是「配置版本」而非乐观锁:更新事务内自增,作为频率去重键的一部分, -- 使修改后的配置能对同一客户重新投放一次,同时旧通知保留原快照不被改写。 -- 2. 范围四维统一用 JSONB 字符串数组保存(pages/shop_ids/device_types/card_types)。 -- 空数组表示该维度未配置即全量;已配置而资产该维度无值时不命中,由应用层与查询共同保证。 -- 3. 受控动作只允许空值、package_purchase、asset_wallet_recharge,禁止任何 URL 或前端路由。 -- 4. 有效期用 starts_at/ends_at 表达,并约束 ends_at >= starts_at。 -- 5. popup_snapshot 可空且只由弹窗投放类型写入,其他通知类型与既有写入路径保持 NULL。 -- 6. 不使用数据库外键,配置与通知、资产之间以 ID 保存并由应用层显式校验。 CREATE TABLE tb_h5_popup_configuration ( id BIGSERIAL PRIMARY KEY, title VARCHAR(100) NOT NULL, content VARCHAR(2000) NOT NULL, pages JSONB NOT NULL DEFAULT '[]', shop_ids JSONB NOT NULL DEFAULT '[]', device_types JSONB NOT NULL DEFAULT '[]', card_types JSONB NOT NULL DEFAULT '[]', priority INTEGER NOT NULL DEFAULT 0, frequency VARCHAR(20) NOT NULL DEFAULT 'once', action_type VARCHAR(40) NOT NULL DEFAULT '', enabled SMALLINT NOT NULL DEFAULT 0, starts_at TIMESTAMPTZ NOT NULL, ends_at TIMESTAMPTZ NOT NULL, version BIGINT NOT NULL DEFAULT 1, 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 chk_h5_popup_configuration_title CHECK (title <> ''), CONSTRAINT chk_h5_popup_configuration_content CHECK (content <> ''), -- 页面必选且非空;其余三维允许空数组(空数组=该维度未配置即全量)。 CONSTRAINT chk_h5_popup_configuration_pages CHECK (jsonb_typeof(pages) = 'array' AND jsonb_array_length(pages) > 0), CONSTRAINT chk_h5_popup_configuration_ranges CHECK ( jsonb_typeof(shop_ids) = 'array' AND jsonb_typeof(device_types) = 'array' AND jsonb_typeof(card_types) = 'array' ), CONSTRAINT chk_h5_popup_configuration_priority CHECK (priority >= 0), CONSTRAINT chk_h5_popup_configuration_frequency CHECK (frequency IN ('once', 'daily')), CONSTRAINT chk_h5_popup_configuration_action_type CHECK (action_type IN ('', 'package_purchase', 'asset_wallet_recharge')), CONSTRAINT chk_h5_popup_configuration_enabled CHECK (enabled IN (0, 1)), CONSTRAINT chk_h5_popup_configuration_period CHECK (ends_at >= starts_at), CONSTRAINT chk_h5_popup_configuration_version CHECK (version > 0) ); -- 候选匹配固定按「启用 → 优先级倒序 → 最近更新时间倒序 → ID 倒序」取最高一条。 -- 启停操作同步刷新 updated_at,因此该索引同时服务启停排序与时间窗口筛选。 -- 本表为后台维护的小型运营配置表,停用走 enabled 置 0 且不做软删除,故索引无条件。 -- 不额外建立 JSONB GIN 索引。 CREATE INDEX idx_h5_popup_configuration_match ON tb_h5_popup_configuration (enabled, priority DESC, updated_at DESC, id DESC); COMMENT ON TABLE tb_h5_popup_configuration IS 'H5 运营弹窗全局配置,仅超级管理员与平台账号维护,客户端只读匹配'; COMMENT ON COLUMN tb_h5_popup_configuration.id IS '主键'; COMMENT ON COLUMN tb_h5_popup_configuration.title IS '弹窗标题,投放时冻结写入通知标题'; COMMENT ON COLUMN tb_h5_popup_configuration.content IS '弹窗正文,投放时冻结写入通知正文,禁止 HTML 与 URL'; COMMENT ON COLUMN tb_h5_popup_configuration.pages IS '命中页面集合 JSONB 数组,必填非空,取值 home/asset_detail/package_purchase/asset_wallet_recharge'; COMMENT ON COLUMN tb_h5_popup_configuration.shop_ids IS '店铺范围 JSONB 字符串数组,空数组表示全量;同维度多选取任一命中'; COMMENT ON COLUMN tb_h5_popup_configuration.device_types IS '设备类型范围 JSONB 字符串数组,空数组表示全量;资产该维度无值时不命中已配置范围'; COMMENT ON COLUMN tb_h5_popup_configuration.card_types IS '卡类型范围 JSONB 字符串数组(CMCC/CUCC/CTCC/CBN),空数组表示全量;资产该维度无值时不命中已配置范围'; COMMENT ON COLUMN tb_h5_popup_configuration.priority IS '优先级,数值越大越优先,仅在启用且时间窗口内参与匹配'; COMMENT ON COLUMN tb_h5_popup_configuration.frequency IS '投放频率 once-每客户每配置版本仅一次 daily-每客户每配置版本每个上海自然日一次'; COMMENT ON COLUMN tb_h5_popup_configuration.action_type IS '受控动作 package_purchase-套餐购买 asset_wallet_recharge-资产钱包充值;空值表示无受控动作,不接受 URL 或前端路由'; COMMENT ON COLUMN tb_h5_popup_configuration.enabled IS '状态 0-禁用 1-启用,启停只影响后续投放且必须刷新 updated_at'; COMMENT ON COLUMN tb_h5_popup_configuration.starts_at IS '生效开始时间'; COMMENT ON COLUMN tb_h5_popup_configuration.ends_at IS '生效结束时间,不得早于开始时间'; COMMENT ON COLUMN tb_h5_popup_configuration.version IS '配置版本,更新事务内递增;旧版本通知保留原快照不被改写'; COMMENT ON COLUMN tb_h5_popup_configuration.creator IS '创建人账号ID,系统写入为 0'; COMMENT ON COLUMN tb_h5_popup_configuration.updater IS '最近更新人账号ID'; COMMENT ON COLUMN tb_h5_popup_configuration.created_at IS '创建时间'; COMMENT ON COLUMN tb_h5_popup_configuration.updated_at IS '最近更新时间,启停与更新均必须刷新'; ALTER TABLE tb_notification ADD COLUMN IF NOT EXISTS popup_snapshot JSONB; -- 快照必须是 JSON 对象,避免写入标量或数组导致读取投影无法解析。 ALTER TABLE tb_notification ADD CONSTRAINT chk_notification_popup_snapshot_object CHECK (popup_snapshot IS NULL OR jsonb_typeof(popup_snapshot) = 'object'); COMMENT ON COLUMN tb_notification.popup_snapshot IS '弹窗投放快照 config_id/config_version/asset_type/asset_id/action_type;仅 h5.popup.risk_exchange 与 h5.popup.operation 写入,其他通知类型保持 NULL';