Files
junhong_cmp_fiber/migrations/000225_add_h5_popup_configuration.up.sql
break 333ba4b647
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m23s
feat(H5弹窗): AUG26-007 风险换卡与运营弹窗投放通知
新增 000225 迁移:运营弹窗配置表 tb_h5_popup_configuration(页面/范围/优先级/频率/受控动作/启停/有效期/版本)
与 tb_notification 可空 JSONB 列 popup_snapshot。

新增通知直建窄接口 DirectWriter.CreateOrGetPersonal:与 Outbox 消费共用 prepareDelivery 的渲染、
展示期与 CreateIdempotent 规则,冲突时回查返回既有行;同步扩展个人通知查询与已读两处类型白名单,
并按个人客户入口补齐投递审计来源。

新增 H5 候选与风险换卡:GET /api/c/v1/popup-candidates 先判风险资格(广电卡 + 风险停机 +
无活动物流换货单),命中只返回风险候选;未命中再按时间/启停/页面/店铺/设备类型/卡类型范围/频率
匹配运营配置。POST /api/c/v1/risk-exchanges/:asset_id/address 锁资产行后幂等创建待发货物流换货单,
首次地址锁定,不沿用资产级群发通知。

新增后台运营弹窗配置 CRUD 与启停(仅超级管理员与平台账号),更新递增版本并刷新最近更新时间,
标题与正文统一拒绝 URL 与前端路由,全部写操作记录操作者、前后值、版本与时间。

同步 OpenAPI(cmd/gendocs、cmd/api/docs.go、pkg/openapi/handlers.go)与参数校验中文提示共用实现。
2026-09-15 15:23:52 +08:00

90 lines
7.0 KiB
SQL
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- 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';