Files
junhong_cmp_fiber/internal/model/dto/h5_popup_dto.go
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

148 lines
15 KiB
Go
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.
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"`
}