feat(H5弹窗): AUG26-007 风险换卡与运营弹窗投放通知
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:
2026-09-15 15:23:52 +08:00
parent 70e680eb0a
commit 333ba4b647
39 changed files with 2861 additions and 166 deletions

View File

@@ -0,0 +1,147 @@
package dto
import "time"
// CreateH5PopupConfigurationRequest 是创建 H5 运营弹窗配置的请求。
// 范围四维中只有 pages 必填;店铺、设备类型、卡类型不传或传空数组表示该维度全量。
// action_type 只接受受控动作白名单,不接受 URL、前端路由或任意参数。
type CreateH5PopupConfigurationRequest struct {
Title string `json:"title" validate:"required,min=1,max=100" required:"true" minLength:"1" maxLength:"100" description:"弹窗标题1100 字符;投放时冻结写入通知标题"`
Content string `json:"content" validate:"required,min=1,max=2000" required:"true" minLength:"1" maxLength:"2000" description:"弹窗正文12000 字符;投放时冻结写入通知正文,不接受 HTML、URL 或前端路由"`
Pages []string `json:"pages" validate:"required,min=1,max=4,dive,oneof=home asset_detail package_purchase asset_wallet_recharge" required:"true" description:"命中页面集合,至少一项 (home:首页, asset_detail:资产详情, package_purchase:套餐购买, asset_wallet_recharge:资产钱包充值)"`
ShopIDs []uint `json:"shop_ids" validate:"omitempty,max=200,dive,min=1" maxLength:"200" description:"店铺范围;不传或空数组表示全量,同维度多选取任一命中"`
DeviceTypes []string `json:"device_types" validate:"omitempty,max=100,dive,min=1,max=50" maxLength:"100" description:"设备类型范围;不传或空数组表示全量;资产该维度无值(独立卡或未绑定设备)时不命中已配置范围"`
CardTypes []string `json:"card_types" validate:"omitempty,max=4,dive,oneof=CMCC CUCC CTCC CBN" maxLength:"4" description:"卡类型范围 (CMCC:中国移动, CUCC:中国联通, CTCC:中国电信, CBN:中国广电);不传或空数组表示全量;非卡资产该维度无值时不命中已配置范围"`
Priority *int `json:"priority" validate:"omitempty,min=0,max=1000000" minimum:"0" maximum:"1000000" description:"优先级,数值越大越优先,默认 0"`
Frequency string `json:"frequency" validate:"required,oneof=once daily" required:"true" enum:"once,daily" description:"投放频率 (once:每客户每配置版本仅一次, daily:每客户每配置版本每个上海自然日一次)"`
Enabled *bool `json:"enabled" description:"是否启用;不传按停用创建"`
ActionType *string `json:"action_type" validate:"omitempty,oneof=package_purchase asset_wallet_recharge" enum:"package_purchase,asset_wallet_recharge" description:"受控动作 (package_purchase:套餐购买, asset_wallet_recharge:资产钱包充值);不传表示无受控动作"`
StartsAt time.Time `json:"starts_at" validate:"required" required:"true" description:"生效开始时间ISO 8601"`
EndsAt time.Time `json:"ends_at" validate:"required" required:"true" description:"生效结束时间ISO 8601不得早于开始时间"`
}
// UpdateH5PopupConfigurationRequest 是更新 H5 运营弹窗配置的请求。
// 除范围集合外均为指针:字段缺省表示保持原值;范围字段传空数组表示该维度改为全量。
// 更新成功即递增配置版本,旧版本已投放通知的内容与快照不被改写。
type UpdateH5PopupConfigurationRequest struct {
Title *string `json:"title" validate:"omitempty,min=1,max=100" minLength:"1" maxLength:"100" description:"弹窗标题1100 字符;不传保持原值"`
Content *string `json:"content" validate:"omitempty,min=1,max=2000" minLength:"1" maxLength:"2000" description:"弹窗正文12000 字符;不传保持原值,不接受 HTML、URL 或前端路由"`
Pages *[]string `json:"pages" validate:"omitempty,min=1,max=4,dive,oneof=home asset_detail package_purchase asset_wallet_recharge" description:"命中页面集合;不传保持原值,传空数组等价于非法(页面必选)"`
ShopIDs *[]uint `json:"shop_ids" validate:"omitempty,max=200,dive,min=1" maxLength:"200" description:"店铺范围;不传保持原值,传空数组表示改为全量"`
DeviceTypes *[]string `json:"device_types" validate:"omitempty,max=100,dive,min=1,max=50" maxLength:"100" description:"设备类型范围;不传保持原值,传空数组表示改为全量"`
CardTypes *[]string `json:"card_types" validate:"omitempty,max=4,dive,oneof=CMCC CUCC CTCC CBN" maxLength:"4" description:"卡类型范围;不传保持原值,传空数组表示改为全量"`
Priority *int `json:"priority" validate:"omitempty,min=0,max=1000000" minimum:"0" maximum:"1000000" description:"优先级,数值越大越优先;不传保持原值"`
Frequency *string `json:"frequency" validate:"omitempty,oneof=once daily" enum:"once,daily" description:"投放频率 (once:每客户每配置版本仅一次, daily:每客户每配置版本每个上海自然日一次);不传保持原值"`
Enabled *bool `json:"enabled" description:"是否启用;不传保持原值。启停会刷新最近更新时间并影响同优先级排序"`
ActionType *string `json:"action_type" validate:"omitempty,oneof=package_purchase asset_wallet_recharge" enum:"package_purchase,asset_wallet_recharge" description:"受控动作;传空字符串表示清除受控动作,不传保持原值"`
StartsAt *time.Time `json:"starts_at" description:"生效开始时间ISO 8601不传保持原值"`
EndsAt *time.Time `json:"ends_at" description:"生效结束时间ISO 8601不传保持原值不得早于开始时间"`
}
// H5PopupConfigurationIDParams 是运营弹窗配置的路径参数。
type H5PopupConfigurationIDParams struct {
ID uint `json:"id" path:"id" required:"true" description:"运营弹窗配置ID"`
}
// UpdateH5PopupConfigurationParams 是更新运营弹窗配置的路径参数与请求体。
// 路径字段必须由 Handler 从 c.Params 回填后再校验,避免 required 恒失败或被请求体覆盖。
type UpdateH5PopupConfigurationParams struct {
ID uint `json:"id" path:"id" required:"true" description:"运营弹窗配置ID"`
UpdateH5PopupConfigurationRequest
}
// H5PopupConfigurationResponse 是运营弹窗配置投影。
type H5PopupConfigurationResponse struct {
ID uint `json:"id" description:"配置ID"`
Title string `json:"title" description:"弹窗标题"`
Content string `json:"content" description:"弹窗正文"`
Pages []string `json:"pages" enums:"home,asset_detail,package_purchase,asset_wallet_recharge" description:"命中页面集合 (home:首页, asset_detail:资产详情, package_purchase:套餐购买, asset_wallet_recharge:资产钱包充值)"`
ShopIDs []uint `json:"shop_ids" description:"店铺范围;空数组表示全量"`
DeviceTypes []string `json:"device_types" description:"设备类型范围;空数组表示全量"`
CardTypes []string `json:"card_types" enums:"CMCC,CUCC,CTCC,CBN" description:"卡类型范围 (CMCC:中国移动, CUCC:中国联通, CTCC:中国电信, CBN:中国广电);空数组表示全量"`
Priority int `json:"priority" description:"优先级,数值越大越优先"`
Frequency string `json:"frequency" enums:"once,daily" description:"投放频率 (once:每客户每配置版本仅一次, daily:每客户每配置版本每个上海自然日一次)"`
FrequencyText string `json:"frequency_text" description:"投放频率名称(中文)"`
Enabled bool `json:"enabled" description:"是否启用;停用后停止新投放,历史通知在展示期内仍可见"`
EnabledText string `json:"enabled_text" description:"启停状态名称(中文)"`
ActionType string `json:"action_type" enums:"package_purchase,asset_wallet_recharge" description:"受控动作 (package_purchase:套餐购买, asset_wallet_recharge:资产钱包充值);空值表示无受控动作"`
StartsAt time.Time `json:"starts_at" description:"生效开始时间ISO 8601"`
EndsAt time.Time `json:"ends_at" description:"生效结束时间ISO 8601"`
Version int64 `json:"version" description:"配置版本,每次更新递增;版本参与频率去重,旧版本通知保留原快照"`
CreatedAt time.Time `json:"created_at" description:"创建时间ISO 8601"`
UpdatedAt time.Time `json:"updated_at" description:"最近更新时间ISO 8601启停同样刷新该时间"`
Creator uint `json:"creator" description:"创建人账号ID"`
Updater uint `json:"updater" description:"最近更新人账号ID"`
}
// H5PopupConfigurationListRequest 是运营弹窗配置列表分页参数。
type H5PopupConfigurationListRequest struct {
Page int `json:"page" query:"page" validate:"omitempty,min=1,max=10000" minimum:"1" maximum:"10000" description:"页码,默认 1最大 10000"`
PageSize int `json:"page_size" query:"page_size" validate:"omitempty,min=1,max=100" minimum:"1" maximum:"100" description:"每页数量,默认 20最大 100"`
Enabled *bool `json:"enabled" query:"enabled" description:"启停筛选;不传时查询全部"`
}
// H5PopupConfigurationListResponse 是运营弹窗配置分页结果。
type H5PopupConfigurationListResponse struct {
Items []H5PopupConfigurationResponse `json:"items" description:"运营弹窗配置列表"`
Total int64 `json:"total" description:"总数量"`
Page int `json:"page" description:"页码"`
Size int `json:"size" description:"每页数量"`
}
// PopupCandidateRequest 是当前客户查询弹窗候选的请求。
// 首页也必须先由客户选定当前资产,因此 identifier 在所有页面都是必填。
type PopupCandidateRequest struct {
Page string `json:"page" query:"page" validate:"required,oneof=home asset_detail package_purchase asset_wallet_recharge" required:"true" enum:"home,asset_detail,package_purchase,asset_wallet_recharge" description:"当前页面 (home:首页, asset_detail:资产详情, package_purchase:套餐购买, asset_wallet_recharge:资产钱包充值)"`
Identifier string `json:"identifier" query:"identifier" validate:"required,min=1,max=100" required:"true" minLength:"1" maxLength:"100" description:"当前资产标识符,卡支持 ICCID、接入号、虚拟号设备支持虚拟号、IMEI、SN"`
}
// PopupCandidateResponse 是弹窗候选结果candidate 为空表示当前页面与资产没有可投放弹窗。
type PopupCandidateResponse struct {
Candidate *PopupCandidateItem `json:"candidate,omitempty" description:"弹窗候选;为空表示当前页面与资产没有可投放弹窗"`
}
// PopupCandidateItem 是当次投放或复用的弹窗候选。
// 只返回类型、资产关联与受控动作,不返回任何 URL 或前端路由;前端按 action_type 白名单映射页面。
type PopupCandidateItem struct {
NotificationID uint `json:"notification_id" description:"投放或复用的站内通知ID关闭或稍后处理时用它调用既有个人通知已读接口"`
PopupType string `json:"popup_type" enums:"risk_exchange,operation" description:"弹窗类型 (risk_exchange:风险换卡, operation:运营弹窗);风险换卡优先级固定高于运营弹窗"`
NotificationType string `json:"notification_type" enums:"h5.popup.risk_exchange,h5.popup.operation" description:"稳定通知类型 (h5.popup.risk_exchange:风险换卡弹窗, h5.popup.operation:运营弹窗)"`
Title string `json:"title" description:"弹窗标题,投放时冻结"`
Body string `json:"body" description:"弹窗正文,投放时冻结"`
AssetType string `json:"asset_type" enums:"iot_card,device" description:"弹窗关联资产类型 (iot_card:物联网卡, device:设备)"`
AssetID uint `json:"asset_id" description:"弹窗关联资产数字ID"`
ActionType string `json:"action_type,omitempty" enums:"package_purchase,asset_wallet_recharge" description:"受控动作 (package_purchase:套餐购买, asset_wallet_recharge:资产钱包充值);为空表示无受控动作"`
ConfigID uint `json:"config_id" description:"运营弹窗配置ID风险换卡弹窗固定为 0"`
ConfigVersion int64 `json:"config_version" description:"投放时的配置版本;风险换卡弹窗固定为 0"`
ExpiresAt *time.Time `json:"expires_at,omitempty" description:"展示截止时间ISO 8601自投放时间起 90 天"`
CreatedAt time.Time `json:"created_at" description:"投放时间ISO 8601"`
}
// ClientRiskExchangeAddressParams 是风险换卡地址提交的路径参数与请求体。
// 地址字段沿用既有换货地址字段与长度校验,不额外拆分省市区。
type ClientRiskExchangeAddressParams struct {
AssetID uint `json:"asset_id" path:"asset_id" required:"true" description:"待换卡资产ID物联网卡数字ID"`
ClientShippingInfoRequest
}
// ClientRiskExchangeResponse 是风险换卡地址提交结果。
// 重复提交返回首次创建的换货单与首次地址,不覆盖既有地址。
type ClientRiskExchangeResponse struct {
ID uint `json:"id" description:"物流换货单ID"`
ExchangeNo string `json:"exchange_no" description:"换货单号"`
Status int `json:"status" description:"换货状态 (2:待发货)"`
StatusName string `json:"status_name" description:"换货状态名称(中文)"`
FlowType string `json:"flow_type" description:"换货流程类型 (shipping:物流换货)"`
OldAssetType string `json:"old_asset_type" description:"旧资产类型 (iot_card:物联网卡)"`
OldAssetID uint `json:"old_asset_id" description:"旧资产ID"`
OldAssetIdentifier string `json:"old_asset_identifier" description:"旧资产权威快照,卡为完整 ICCID"`
RecipientName string `json:"recipient_name" description:"收件人姓名,首次提交后锁定"`
RecipientPhone string `json:"recipient_phone" description:"收件人电话,首次提交后锁定"`
RecipientAddress string `json:"recipient_address" description:"收货地址,首次提交后锁定,客户不可修改"`
MigrateData bool `json:"migrate_data" description:"是否执行全量迁移;风险换卡固定 false发货选新资产时仍由后台按既有流程决定"`
MigrationStatus string `json:"migration_status" enums:"not_migrated,pending,migrated,failed" description:"业务数据迁移状态 (not_migrated:不迁移, pending:待迁移, migrated:已迁移, failed:迁移失败);迁移结果以本字段为准"`
MigrationStatusName string `json:"migration_status_name" description:"业务数据迁移状态名称(中文)"`
ExchangeReason string `json:"exchange_reason" description:"换货原因"`
CreatedAt time.Time `json:"created_at" description:"创建时间ISO 8601"`
}

View File

@@ -11,7 +11,7 @@ type NotificationUnreadCountResponse struct {
// NotificationListRequest 是后台通知基础分页参数。
type NotificationListRequest struct {
Category string `json:"category" query:"category" validate:"omitempty,oneof=approval expiry sync system" enums:"approval,expiry,sync,system" description:"通知类别 (approval:审批, expiry:临期, sync:同步, system:系统)"`
Type string `json:"type" query:"type" validate:"omitempty,oneof=system.notice package.expiring agent.recharge.completed refund.completed exchange.shipping.created agent.main_wallet.low_balance" enums:"system.notice,package.expiring,agent.recharge.completed,refund.completed,exchange.shipping.created,agent.main_wallet.low_balance" description:"稳定通知类型 (system.notice:系统通知, package.expiring:套餐临期, agent.recharge.completed:店铺充值入账, refund.completed:店铺退款完成, exchange.shipping.created:换货申请待处理, agent.main_wallet.low_balance:主钱包低余额)"`
Type string `json:"type" query:"type" validate:"omitempty,oneof=system.notice package.expiring agent.recharge.completed refund.completed exchange.shipping.created agent.main_wallet.low_balance h5.popup.risk_exchange h5.popup.operation" enums:"system.notice,package.expiring,agent.recharge.completed,refund.completed,exchange.shipping.created,agent.main_wallet.low_balance,h5.popup.risk_exchange,h5.popup.operation" description:"稳定通知类型 (system.notice:系统通知, package.expiring:套餐临期, agent.recharge.completed:店铺充值入账, refund.completed:店铺退款完成, exchange.shipping.created:换货申请待处理, agent.main_wallet.low_balance:主钱包低余额, h5.popup.risk_exchange:风险换卡弹窗, h5.popup.operation:运营弹窗)"`
Severity string `json:"severity" query:"severity" validate:"omitempty,oneof=info warning error critical" enums:"info,warning,error,critical" description:"通知级别 (info:提示, warning:警告, error:错误, critical:严重)"`
IsRead *bool `json:"is_read" query:"is_read" description:"已读状态;不传时查询全部"`
Page int `json:"page" query:"page" validate:"omitempty,min=1,max=10000" minimum:"1" maximum:"10000" description:"页码,默认 1最大 10000"`
@@ -22,7 +22,7 @@ type NotificationListRequest struct {
type NotificationItem struct {
ID uint `json:"id" description:"通知ID"`
Category string `json:"category" enums:"approval,expiry,sync,system" description:"通知类别 (approval:审批, expiry:临期, sync:同步, system:系统)"`
Type string `json:"type" enums:"system.notice,package.expiring,agent.recharge.completed,refund.completed,exchange.shipping.created,agent.main_wallet.low_balance" description:"稳定通知类型 (system.notice:系统通知, package.expiring:套餐临期, agent.recharge.completed:店铺充值入账, refund.completed:店铺退款完成, exchange.shipping.created:换货申请待处理, agent.main_wallet.low_balance:主钱包低余额)"`
Type string `json:"type" enums:"system.notice,package.expiring,agent.recharge.completed,refund.completed,exchange.shipping.created,agent.main_wallet.low_balance,h5.popup.risk_exchange,h5.popup.operation" description:"稳定通知类型 (system.notice:系统通知, package.expiring:套餐临期, agent.recharge.completed:店铺充值入账, refund.completed:店铺退款完成, exchange.shipping.created:换货申请待处理, agent.main_wallet.low_balance:主钱包低余额, h5.popup.risk_exchange:风险换卡弹窗, h5.popup.operation:运营弹窗)"`
Severity string `json:"severity" enums:"info,warning,error,critical" description:"通知级别 (info:提示, warning:警告, error:错误, critical:严重)"`
Title string `json:"title" description:"纯文本标题"`
Body string `json:"body" description:"纯文本正文"`
@@ -32,6 +32,17 @@ type NotificationItem struct {
IsRead bool `json:"is_read" description:"是否已读"`
ReadAt *time.Time `json:"read_at,omitempty" description:"首次已读时间ISO 8601"`
CreatedAt time.Time `json:"created_at" description:"创建时间ISO 8601"`
// PopupSnapshot 仅弹窗投放类型返回;其他通知类型为空。只包含配置标识、资产关联与受控动作,不含任何 URL 或前端路由。
PopupSnapshot *NotificationPopupSnapshotItem `json:"popup_snapshot,omitempty" description:"弹窗投放快照;仅 h5.popup.risk_exchange 与 h5.popup.operation 返回,其他通知类型为空"`
}
// NotificationPopupSnapshotItem 是弹窗投放通知冻结的快照投影,不含任何 URL 或前端路由。
type NotificationPopupSnapshotItem struct {
ConfigID uint `json:"config_id" description:"运营弹窗配置ID风险换卡弹窗没有配置固定为 0"`
ConfigVersion int64 `json:"config_version" description:"投放时的配置版本;风险换卡弹窗固定为 0。旧版本通知保留原快照不被改写"`
AssetType string `json:"asset_type" enums:"iot_card,device" description:"弹窗关联资产类型 (iot_card:物联网卡, device:设备)"`
AssetID uint `json:"asset_id" description:"弹窗关联资产数字ID"`
ActionType string `json:"action_type,omitempty" enums:"package_purchase,asset_wallet_recharge" description:"受控动作 (package_purchase:套餐购买, asset_wallet_recharge:资产钱包充值);为空表示无受控动作。前端按白名单映射页面,不得由后端下发 URL 或前端路由"`
}
// NotificationListResponse 是后台通知基础分页结果。

View File

@@ -0,0 +1,69 @@
package model
import (
"database/sql/driver"
"time"
"github.com/bytedance/sonic"
)
// H5PopupConfiguration 是 H5 运营弹窗全局配置的 PostgreSQL 持久化事实。
// 配置只决定后续投放:启停与有效期不删除行,停用走 Enabled 置 0
// Version 是配置版本而非乐观锁,更新事务内递增并参与频率去重键,旧版本通知保留原快照不被改写。
// 范围四维统一用 JSONB 字符串数组:空数组表示该维度未配置即全量,已配置而资产该维度无值时不命中。
type H5PopupConfiguration struct {
ID uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
Title string `gorm:"column:title;type:varchar(100);not null" json:"title"`
Content string `gorm:"column:content;type:varchar(2000);not null" json:"content"`
Pages StringJSONBArray `gorm:"column:pages;type:jsonb;not null;default:'[]'" json:"pages"`
ShopIDs StringJSONBArray `gorm:"column:shop_ids;type:jsonb;not null;default:'[]'" json:"shop_ids"`
DeviceTypes StringJSONBArray `gorm:"column:device_types;type:jsonb;not null;default:'[]'" json:"device_types"`
CardTypes StringJSONBArray `gorm:"column:card_types;type:jsonb;not null;default:'[]'" json:"card_types"`
Priority int `gorm:"column:priority;type:integer;not null;default:0" json:"priority"`
Frequency string `gorm:"column:frequency;type:varchar(20);not null;default:'once'" json:"frequency"`
ActionType string `gorm:"column:action_type;type:varchar(40);not null;default:''" json:"action_type"`
Enabled int `gorm:"column:enabled;type:smallint;not null;default:0" json:"enabled"`
StartsAt time.Time `gorm:"column:starts_at;type:timestamptz;not null" json:"starts_at"`
EndsAt time.Time `gorm:"column:ends_at;type:timestamptz;not null" json:"ends_at"`
Version int64 `gorm:"column:version;type:bigint;not null;default:1" json:"version"`
BaseModel `gorm:"embedded"`
CreatedAt time.Time `gorm:"column:created_at;type:timestamptz;not null;autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamptz;not null;autoUpdateTime" json:"updated_at"`
}
// TableName 返回 H5 运营弹窗配置表名。
func (H5PopupConfiguration) TableName() string {
return "tb_h5_popup_configuration"
}
// NotificationPopupSnapshot 是弹窗投放通知冻结的投放快照,作为可空 JSONB 写入 tb_notification。
// 只承载配置标识与受控动作,不含任何 URL 或前端路由风险换卡弹窗没有配置ConfigID 与 ConfigVersion 为 0。
type NotificationPopupSnapshot struct {
ConfigID uint `json:"config_id"`
ConfigVersion int64 `json:"config_version"`
AssetType string `json:"asset_type"`
AssetID uint `json:"asset_id"`
ActionType string `json:"action_type"`
}
// Value 将弹窗快照序列化为 JSONBnil 接收者写入 NULL供非弹窗通知保持该列为空。
func (s *NotificationPopupSnapshot) Value() (driver.Value, error) {
if s == nil {
return nil, nil
}
return sonic.Marshal(s)
}
// Scan 从 JSONB 读取弹窗快照;数据库 NULL 时清空快照。
func (s *NotificationPopupSnapshot) Scan(value any) error {
if value == nil {
*s = NotificationPopupSnapshot{}
return nil
}
data, ok := value.([]byte)
if !ok {
*s = NotificationPopupSnapshot{}
return nil
}
return sonic.Unmarshal(data, s)
}

View File

@@ -20,6 +20,8 @@ type Notification struct {
ReadAt *time.Time `gorm:"column:read_at;type:timestamptz" json:"read_at,omitempty"`
ExpiresAt *time.Time `gorm:"column:expires_at;type:timestamptz" json:"expires_at,omitempty"`
CreatedAt time.Time `gorm:"column:created_at;type:timestamptz;not null;autoCreateTime" json:"created_at"`
// PopupSnapshot 只由弹窗投放类型h5.popup.risk_exchange、h5.popup.operation写入其他通知类型保持 NULL。
PopupSnapshot *NotificationPopupSnapshot `gorm:"column:popup_snapshot;type:jsonb" json:"popup_snapshot,omitempty"`
}
// TableName 返回站内通知表名。