feat(H5弹窗): AUG26-007 风险换卡与运营弹窗投放通知
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m23s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m23s
新增 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)与参数校验中文提示共用实现。
This commit is contained in:
17
migrations/000225_add_h5_popup_configuration.down.sql
Normal file
17
migrations/000225_add_h5_popup_configuration.down.sql
Normal file
@@ -0,0 +1,17 @@
|
||||
-- 回滚 H5 运营弹窗配置与弹窗投放快照。
|
||||
-- 先删快照约束与快照列,再删配置表与索引,与 up 的创建顺序成对;既有通知列、类别 CHECK 与保留清理不受影响。
|
||||
--
|
||||
-- 不可逆说明(ENG-MIG-001 例外条件):本迁移的 down 会删除 tb_notification.popup_snapshot
|
||||
-- 与 tb_h5_popup_configuration。恢复方式为重新执行 up 重建结构;被删除的弹窗快照与配置行
|
||||
-- 无法由数据库自身重建,只能依据 tb_audit_event 中 h5_popup_configuration 的配置审计前后值与
|
||||
-- 已投递通知正文人工复核,因此仅在确认不再需要弹窗投放事实时才执行 down。
|
||||
|
||||
ALTER TABLE tb_notification
|
||||
DROP CONSTRAINT IF EXISTS chk_notification_popup_snapshot_object;
|
||||
|
||||
ALTER TABLE tb_notification
|
||||
DROP COLUMN IF EXISTS popup_snapshot;
|
||||
|
||||
DROP INDEX IF EXISTS idx_h5_popup_configuration_match;
|
||||
|
||||
DROP TABLE IF EXISTS tb_h5_popup_configuration;
|
||||
89
migrations/000225_add_h5_popup_configuration.up.sql
Normal file
89
migrations/000225_add_h5_popup_configuration.up.sql
Normal file
@@ -0,0 +1,89 @@
|
||||
-- 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';
|
||||
Reference in New Issue
Block a user