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:
147
internal/model/dto/h5_popup_dto.go
Normal file
147
internal/model/dto/h5_popup_dto.go
Normal 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:"弹窗标题,1~100 字符;投放时冻结写入通知标题"`
|
||||
Content string `json:"content" validate:"required,min=1,max=2000" required:"true" minLength:"1" maxLength:"2000" description:"弹窗正文,1~2000 字符;投放时冻结写入通知正文,不接受 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:"弹窗标题,1~100 字符;不传保持原值"`
|
||||
Content *string `json:"content" validate:"omitempty,min=1,max=2000" minLength:"1" maxLength:"2000" description:"弹窗正文,1~2000 字符;不传保持原值,不接受 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)"`
|
||||
}
|
||||
@@ -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 是后台通知基础分页结果。
|
||||
|
||||
69
internal/model/h5_popup_configuration.go
Normal file
69
internal/model/h5_popup_configuration.go
Normal 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 将弹窗快照序列化为 JSONB;nil 接收者写入 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)
|
||||
}
|
||||
@@ -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 返回站内通知表名。
|
||||
|
||||
Reference in New Issue
Block a user