Files
junhong_cmp_fiber/internal/model/asset_auto_renewal.go
break d52be16802
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 10m15s
feat(资产钱包自动续费): 新增全局配置、每日扫描续购与可靠复机
- 新增单行配置表 tb_asset_auto_renewal_config 与尝试记录表 tb_asset_auto_renewal_attempt(迁移 000229/000230)
- 每日按上海自然日扫描,窗口内以同一资产钱包可用余额续购当前主套餐,资金/订单/套餐/审计同一事务闭合
- 唯一键保证每资产每日至多一次尝试,占位中断由后续扫描收敛,当日不重试
- 四类失败原因向客户与店铺各投递每日至多一条站内通知,并注册通知类型与个人客户白名单
- 续费成功后按条件经 Outbox 可靠投递复机,新增恢复扫描只查询回填,不使用即发即弃调用
- 配置读写仅超级管理员与平台账号,保存记录操作者、前后值快照并登记统一审计
- tasks 7.1–7.15 全部验证通过(本机隔离 PostgreSQL/Redis,零外部渠道调用)
2026-09-17 16:39:26 +08:00

100 lines
8.5 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 model
import (
"time"
"gorm.io/gorm"
)
// AssetAutoRenewalConfig 是全局唯一一行自动续费配置的 PostgreSQL 持久化事实。
//
// 主键恒为 1数据库 CHECK 约束保证单行):配置只有一份全局生效值,不按店铺、企业或个人
// 客户分范围。ConfigVersion 是配置版本而非乐观锁,保存事务内递增并供尝试记录冻结快照,
// 已产生的尝试记录保留原版本、不重算。PackageIDs 只在 Scope 为 specified 时非空。
type AssetAutoRenewalConfig struct {
ID uint `gorm:"column:id;primaryKey" json:"id"`
Enabled int `gorm:"column:enabled;type:smallint;not null;default:0;comment:总开关 0-关闭 1-开启" json:"enabled"`
Scope string `gorm:"column:scope;type:varchar(16);not null;default:'all';comment:适用范围 all-全部主套餐 specified-指定主套餐" json:"scope"`
PackageIDs UintJSONBArray `gorm:"column:package_ids;type:jsonb;not null;default:'[]';comment:指定主套餐集合(仅 specified 范围非空)" json:"package_ids"`
DaysBeforeExpiry int `gorm:"column:days_before_expiry;type:integer;not null;default:15;comment:统一到期前天数1-90" json:"days_before_expiry"`
ConfigVersion int64 `gorm:"column:config_version;type:bigint;not null;default:1;comment:配置版本,保存事务内自增" json:"config_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 返回自动续费配置表名。
func (AssetAutoRenewalConfig) TableName() string {
return "tb_asset_auto_renewal_config"
}
// AssetAutoRenewalAttempt 是一次自动续费尝试的 PostgreSQL 持久化事实。
//
// 一行表达一个 (AssetType, AssetID, TriggerDate) 组合,由部分唯一索引保证每项资产每日至多
// 一次尝试Status 为终态(成功/失败/跳过)时该次尝试已收敛,仍为处理中表示占位后进程中断。
// 资金、订单、复机与配置窗口字段全部是触发时冻结的快照,不随后续配置或资产变化重算。
type AssetAutoRenewalAttempt struct {
gorm.Model
// AssetType 取值与资产钱包资源类型一致iot_card / device卡与设备分别计数。
AssetType string `gorm:"column:asset_type;type:varchar(20);not null;comment:资产类型 iot_card-物联网卡 device-设备" json:"asset_type"`
AssetID uint `gorm:"column:asset_id;type:bigint;not null;comment:资产ID" json:"asset_id"`
// TriggerDate 是触发日的上海自然日,与资产类型、资产 ID 共同构成唯一键。
TriggerDate time.Time `gorm:"column:trigger_date;type:date;not null;comment:触发日期(上海自然日)" json:"trigger_date"`
// Status 取值 constants.AssetAutoRenewalAttemptStatus*。
Status int `gorm:"column:status;type:smallint;not null;default:1;comment:尝试状态 1-处理中 2-成功 3-失败 4-跳过" json:"status"`
// FailureReason 取值 constants.AssetAutoRenewalFailure*,成功与跳过时为空。
FailureReason string `gorm:"column:failure_reason;type:varchar(32);not null;default:'';comment:失败原因 insufficient_balance-余额不足 not_renewable-不可续费 order_failed-订单失败 resume_failed-复机失败" json:"failure_reason"`
// FailureDetail 是可安全记录的失败说明,不写渠道报文、凭证或内部错误细节。
FailureDetail string `gorm:"column:failure_detail;type:varchar(500);not null;default:'';comment:可安全展示的失败说明" json:"failure_detail"`
// SkipReason 取值 constants.AssetAutoRenewalSkip*,仅跳过态有值。
SkipReason string `gorm:"column:skip_reason;type:varchar(32);not null;default:'';comment:跳过原因 manual_renewed-人工已完成续购 manual_order_pending-人工订单在途" json:"skip_reason"`
// 触发时冻结的客户与店铺快照。
CustomerID uint `gorm:"column:customer_id;type:bigint;not null;default:0;comment:触发时解析到的当前个人客户ID0-无" json:"customer_id"`
ShopID *uint `gorm:"column:shop_id;type:bigint;comment:触发时资产所属店铺IDNULL-无店铺" json:"shop_id"`
// 触发时冻结的配置与窗口快照。
ConfigVersion int64 `gorm:"column:config_version;type:bigint;not null;default:0;comment:触发时配置版本快照" json:"config_version"`
WindowDays int `gorm:"column:window_days;type:integer;not null;default:0;comment:触发时到期前天数快照" json:"window_days"`
FinalExpiresAt *time.Time `gorm:"column:final_expires_at;type:timestamptz;comment:触发时最终到期时间快照" json:"final_expires_at,omitempty"`
// 当前主套餐与续购对象。
CurrentUsageID uint `gorm:"column:current_usage_id;type:bigint;not null;default:0;comment:触发时当前主套餐使用记录ID" json:"current_usage_id"`
CurrentPackageID uint `gorm:"column:current_package_id;type:bigint;not null;default:0;comment:触发时当前套餐商品ID" json:"current_package_id"`
RenewPackageID uint `gorm:"column:renew_package_id;type:bigint;not null;default:0;comment:待续购套餐商品ID" json:"renew_package_id"`
RenewPrice int64 `gorm:"column:renew_price;type:bigint;not null;default:0;comment:执行时当前可售续费价(分)" json:"renew_price"`
// 资金事实。
WalletID uint `gorm:"column:wallet_id;type:bigint;not null;default:0;comment:扣款资产钱包ID" json:"wallet_id"`
// WalletTransactionID 即规格中的钱包流水号,指向 tb_asset_wallet_transaction.id。
WalletTransactionID uint `gorm:"column:wallet_transaction_id;type:bigint;not null;default:0;comment:资产钱包流水标识tb_asset_wallet_transaction.id" json:"wallet_transaction_id"`
DeductAmount int64 `gorm:"column:deduct_amount;type:bigint;not null;default:0;comment:扣款金额(分)" json:"deduct_amount"`
BalanceBefore int64 `gorm:"column:balance_before;type:bigint;not null;default:0;comment:扣款前钱包余额(分)" json:"balance_before"`
BalanceAfter int64 `gorm:"column:balance_after;type:bigint;not null;default:0;comment:扣款后钱包余额(分)" json:"balance_after"`
// 订单事实。
OrderID uint `gorm:"column:order_id;type:bigint;not null;default:0;comment:续费订单ID" json:"order_id"`
OrderNo string `gorm:"column:order_no;type:varchar(64);not null;default:'';comment:续费订单号快照" json:"order_no"`
// 复机事实。
ResumeStatus int `gorm:"column:resume_status;type:smallint;not null;default:0;comment:复机状态 0-未评估 1-跳过 2-已投递 3-成功 4-失败 5-未知" json:"resume_status"`
// ResumeSubmittedAt 是复机执行提交认领时刻:消费者以「为空」条件更新取得至多一次的外部调用权。
ResumeSubmittedAt *time.Time `gorm:"column:resume_submitted_at;type:timestamptz;comment:复机执行提交认领时刻" json:"resume_submitted_at,omitempty"`
// ResumeIntegrationID 记录复机 Gateway 调用的 Integration Log 标识,便于人工核对。
ResumeIntegrationID string `gorm:"column:resume_integration_id;type:varchar(64);not null;default:'';comment:复机外部交互标识Integration Log 标识)" json:"resume_integration_id"`
ResumeFailureReason string `gorm:"column:resume_failure_reason;type:varchar(500);not null;default:'';comment:可安全展示的复机失败原因" json:"resume_failure_reason"`
// ResumeAnomalyFlag 为 1 表示查询窗口超期仍无法确认,退出自动扫描转人工核对。
ResumeAnomalyFlag int `gorm:"column:resume_anomaly_flag;type:smallint;not null;default:0;comment:复机异常标记 0-正常 1-需人工核对" json:"resume_anomaly_flag"`
// 操作者与跨日尝试次数:本能力无人工处理入口,操作者恒为系统任务。
OperatorType string `gorm:"column:operator_type;type:varchar(32);not null;default:'system_task';comment:操作者类型,恒为 system_task" json:"operator_type"`
OperatorID string `gorm:"column:operator_id;type:varchar(64);not null;default:'';comment:操作者标识,系统任务固定为计划任务类型" json:"operator_id"`
// AttemptSeq 是该资产截至本次尝试当日的跨日累计尝试次数。
AttemptSeq int `gorm:"column:attempt_seq;type:integer;not null;default:1;comment:跨日尝试次数" json:"attempt_seq"`
}
// TableName 返回自动续费尝试表名。
func (AssetAutoRenewalAttempt) TableName() string {
return "tb_asset_auto_renewal_attempt"
}