feat(资产钱包自动续费): 新增全局配置、每日扫描续购与可靠复机
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 10m15s

- 新增单行配置表 tb_asset_auto_renewal_config 与尝试记录表 tb_asset_auto_renewal_attempt(迁移 000229/000230)
- 每日按上海自然日扫描,窗口内以同一资产钱包可用余额续购当前主套餐,资金/订单/套餐/审计同一事务闭合
- 唯一键保证每资产每日至多一次尝试,占位中断由后续扫描收敛,当日不重试
- 四类失败原因向客户与店铺各投递每日至多一条站内通知,并注册通知类型与个人客户白名单
- 续费成功后按条件经 Outbox 可靠投递复机,新增恢复扫描只查询回填,不使用即发即弃调用
- 配置读写仅超级管理员与平台账号,保存记录操作者、前后值快照并登记统一审计
- tasks 7.1–7.15 全部验证通过(本机隔离 PostgreSQL/Redis,零外部渠道调用)
This commit is contained in:
2026-09-17 16:39:26 +08:00
parent 70e6b186df
commit d52be16802
54 changed files with 5164 additions and 97 deletions

View File

@@ -0,0 +1,167 @@
package constants
import "time"
// 资产钱包自动续费配置适用范围。
const (
// AssetAutoRenewalScopeAll 表示配置适用于全部主套餐。
AssetAutoRenewalScopeAll = "all"
// AssetAutoRenewalScopeSpecified 表示配置只适用于指定主套餐集合。
AssetAutoRenewalScopeSpecified = "specified"
)
// 资产钱包自动续费配置的结构约束。
const (
// AssetAutoRenewalConfigSingletonID 是单行配置表的主键,恒为 1。
AssetAutoRenewalConfigSingletonID = 1
// AssetAutoRenewalConfigEnabledOff 表示总开关关闭。
AssetAutoRenewalConfigEnabledOff = 0
// AssetAutoRenewalConfigEnabledOn 表示总开关开启。
AssetAutoRenewalConfigEnabledOn = 1
// AssetAutoRenewalMinDaysBeforeExpiry 是到期前天数的最小值。
AssetAutoRenewalMinDaysBeforeExpiry = 1
// AssetAutoRenewalMaxDaysBeforeExpiry 是到期前天数的上限。
AssetAutoRenewalMaxDaysBeforeExpiry = 90
)
// 资产钱包自动续费尝试状态ENG-STATE-001生命周期状态使用整数编码
const (
// AssetAutoRenewalAttemptStatusProcessing 表示尝试已占位、执行尚未得出终态。
AssetAutoRenewalAttemptStatusProcessing = 1
// AssetAutoRenewalAttemptStatusSucceeded 表示本次尝试已完成续费。
AssetAutoRenewalAttemptStatusSucceeded = 2
// AssetAutoRenewalAttemptStatusFailed 表示本次尝试以失败终态结束。
AssetAutoRenewalAttemptStatusFailed = 3
// AssetAutoRenewalAttemptStatusSkipped 表示本次尝试因人工优先而跳过。
AssetAutoRenewalAttemptStatusSkipped = 4
)
// 资产钱包自动续费通知原因,固定为四类。
//
// 前三类由尝试失败产生通知(写入尝试记录的 failure_reason
// 第四类复机失败只由复机子结果resume_status=失败)产生通知,
// 绝不回写尝试记录的 failure_reason——复机结果只在复机字段表达同一事实不两处表达。
const (
// AssetAutoRenewalFailureInsufficientBalance 表示可用余额不支持续购。
AssetAutoRenewalFailureInsufficientBalance = "insufficient_balance"
// AssetAutoRenewalFailureNotRenewable 表示当前条件不允许自动续购。
AssetAutoRenewalFailureNotRenewable = "not_renewable"
// AssetAutoRenewalFailureOrderFailed 表示续购事务本身失败并整体回滚。
AssetAutoRenewalFailureOrderFailed = "order_failed"
// AssetAutoRenewalFailureResumeFailed 表示复机最终确认失败(只作为通知原因,不写 failure_reason
AssetAutoRenewalFailureResumeFailed = "resume_failed"
)
// 资产钱包自动续费尝试记录中允许出现的失败归类。
//
// 取值域是通知原因中的前三类加上中断收敛值:失败归类只描述「实际进入执行后得出的失败」,
// 因此不含复机失败。interrupted 是占位后进程中断的收敛表示,不属于四类通知原因,
// 由 IsAssetAutoRenewalNotifiableFailureReason 显式排除,收敛过程不发送任何通知。
const (
// AssetAutoRenewalFailureInterrupted 表示占位写入后进程中断,由后续扫描收敛。
AssetAutoRenewalFailureInterrupted = "interrupted"
)
// IsAssetAutoRenewalNotifiableFailureReason 判断该失败原因是否触发站内通知。
// 未登记的原因一律不通知fail-closed避免把收敛产生的 interrupted 误当执行失败通知出去。
func IsAssetAutoRenewalNotifiableFailureReason(reason string) bool {
switch reason {
case AssetAutoRenewalFailureInsufficientBalance, AssetAutoRenewalFailureNotRenewable,
AssetAutoRenewalFailureOrderFailed, AssetAutoRenewalFailureResumeFailed:
return true
default:
return false
}
}
// 资产钱包自动续费跳过原因。
const (
// AssetAutoRenewalSkipManualRenewed 表示人工已完成续购(已存在待生效主套餐)。
AssetAutoRenewalSkipManualRenewed = "manual_renewed"
// AssetAutoRenewalSkipManualOrderPending 表示该资产存在未关闭的人工主套餐订单。
AssetAutoRenewalSkipManualOrderPending = "manual_order_pending"
)
// 资产钱包自动续费尝试的复机状态。
const (
// AssetAutoRenewalResumeStatusSkipped 表示可复机条件不成立,未投递复机。
AssetAutoRenewalResumeStatusSkipped = 1
// AssetAutoRenewalResumeStatusRequested 表示已可靠投递复机请求,等待结果。
AssetAutoRenewalResumeStatusRequested = 2
// AssetAutoRenewalResumeStatusSucceeded 表示复机已确认成功。
AssetAutoRenewalResumeStatusSucceeded = 3
// AssetAutoRenewalResumeStatusFailed 表示复机已确认失败。
AssetAutoRenewalResumeStatusFailed = 4
// AssetAutoRenewalResumeStatusUnknown 表示复机结果未知,等待恢复扫描确认。
AssetAutoRenewalResumeStatusUnknown = 5
)
// 资产钱包自动续费尝试的操作者类型:本能力无人工处理入口,恒为系统任务。
const (
// AssetAutoRenewalOperatorTypeSystemTask 表示系统任务操作者。
AssetAutoRenewalOperatorTypeSystemTask = "system_task"
)
// 资产钱包自动续费尝试的批次与查询约束。
const (
// AssetAutoRenewalRecoveryBatchSize 是恢复扫描单批处理条数上限。
AssetAutoRenewalRecoveryBatchSize = 200
// AssetAutoRenewalConfigDisplayName 是配置资源的展示名。
AssetAutoRenewalConfigDisplayName = "资产钱包自动续费配置"
)
// 资产钱包自动续费的可靠事件类型。
const (
// OutboxEventTypeAssetAutoRenewalResumeRequested 表示续费成功后按条件触发的可靠复机请求。
OutboxEventTypeAssetAutoRenewalResumeRequested = "asset_auto_renewal.resume.requested"
)
// 资产钱包自动续费复机子任务的查询窗口。
const (
// AssetAutoRenewalResumeQueryWindow 是复机子任务自提交起允许自动查询确认的窗口。
// 语义:复机事件进入已投递/已提交后,恢复扫描只查询运营商状态回填;超过该窗口仍无法确认的
// 尝试会标记异常并转人工核对,不再重复查询同一笔无法收敛的结果,也不回滚任何续费事实。
AssetAutoRenewalResumeQueryWindow = 30 * time.Minute
)
// GetAssetAutoRenewalScopeName 返回自动续费适用范围的中文名称。
func GetAssetAutoRenewalScopeName(scope string) string {
switch scope {
case AssetAutoRenewalScopeAll:
return "全部主套餐"
case AssetAutoRenewalScopeSpecified:
return "指定主套餐"
default:
return "未知"
}
}
// GetAssetAutoRenewalFailureReasonName 返回自动续费失败原因的中文名称。
func GetAssetAutoRenewalFailureReasonName(reason string) string {
switch reason {
case AssetAutoRenewalFailureInsufficientBalance:
return "余额不足"
case AssetAutoRenewalFailureNotRenewable:
return "不可续费"
case AssetAutoRenewalFailureOrderFailed:
return "订单失败"
case AssetAutoRenewalFailureResumeFailed:
return "复机失败"
case AssetAutoRenewalFailureInterrupted:
return "中断收敛"
default:
return "未知"
}
}
// GetAssetAutoRenewalSkipReasonName 返回自动续费跳过原因的中文名称。
func GetAssetAutoRenewalSkipReasonName(reason string) string {
switch reason {
case AssetAutoRenewalSkipManualRenewed:
return "人工已完成续购"
case AssetAutoRenewalSkipManualOrderPending:
return "人工订单在途"
default:
return "未知"
}
}

View File

@@ -556,6 +556,12 @@ const (
AuditActionPackageTrafficAlertRuleDisabled = "package_traffic_alert_rule.disable"
// AuditActionPackageTrafficAlertTriggered 表示扫描命中阈值时创建套餐真流量达量预警事实。
AuditActionPackageTrafficAlertTriggered = "package_traffic_alert.trigger"
// AuditActionAssetAutoRenewalConfigUpdated 表示保存受控的资产钱包自动续费配置。
AuditActionAssetAutoRenewalConfigUpdated = "asset_auto_renewal.config.updated"
// AuditActionAssetAutoRenewalRenewed 表示自动续费在同一事务内完成续购并置尝试成功。
AuditActionAssetAutoRenewalRenewed = "asset_auto_renewal.renewed"
// AuditActionAssetAutoRenewalFailed 表示自动续费以失败或跳过终态结束。
AuditActionAssetAutoRenewalFailed = "asset_auto_renewal.failed"
// AuditActionNotificationDelivered 表示 Outbox 消费后实际生成站内通知。
AuditActionNotificationDelivered = "notification.deliver"
// AuditActionNotificationRead 表示单条通知首次标记已读。
@@ -634,6 +640,8 @@ const (
AuditActionPermissionDeleted = "permission.delete"
// AuditOperationSystemConfigUpdate 表示系统配置旧接缝传入的操作类型。
AuditOperationSystemConfigUpdate = "system_config_update"
// AuditOperationAssetAutoRenewalConfigUpdate 表示资产钱包自动续费配置旧接缝传入的操作类型。
AuditOperationAssetAutoRenewalConfigUpdate = "asset_auto_renewal_config_update"
// AuditOperationPaymentConfigCreate 表示创建支付连接配置。
AuditOperationPaymentConfigCreate = "payment_config_create"
// AuditOperationPaymentConfigUpdate 表示更新支付连接配置。
@@ -731,6 +739,10 @@ const (
AuditResourcePackageTrafficAlertRule = "package_traffic_alert_rule"
// AuditResourcePackageTrafficAlert 表示套餐真流量达量预警事实资源。
AuditResourcePackageTrafficAlert = "package_traffic_alert"
// AuditResourceAssetAutoRenewalConfig 表示资产钱包自动续费全局配置资源。
AuditResourceAssetAutoRenewalConfig = "asset_auto_renewal_config"
// AuditResourceAssetAutoRenewalAttempt 表示资产钱包自动续费尝试记录资源。
AuditResourceAssetAutoRenewalAttempt = "asset_auto_renewal_attempt"
// AuditResourceNotification 表示站内通知资源。
AuditResourceNotification = "notification"
// AuditResourceNotificationReadBatch 表示通知批量已读资源。
@@ -879,6 +891,16 @@ const (
AuditResourceRolePackageTrafficAlertRuleTarget = "package_traffic_alert_rule_target"
// AuditResourceRolePackageTrafficAlertTarget 表示套餐真流量达量预警事实资源。
AuditResourceRolePackageTrafficAlertTarget = "package_traffic_alert_target"
// AuditResourceRoleAssetAutoRenewalConfigTarget 表示资产钱包自动续费配置主资源。
AuditResourceRoleAssetAutoRenewalConfigTarget = "asset_auto_renewal_config_target"
// AuditResourceRoleAssetAutoRenewalAttemptTarget 表示本次自动续费尝试记录主资源。
AuditResourceRoleAssetAutoRenewalAttemptTarget = "asset_auto_renewal_attempt_target"
// AuditResourceRoleAssetAutoRenewalWallet 表示本次自动续费实际扣款的资产钱包。
AuditResourceRoleAssetAutoRenewalWallet = "asset_auto_renewal_wallet"
// AuditResourceRoleAssetAutoRenewalWalletTransaction 表示本次自动续费产生的资产钱包流水。
AuditResourceRoleAssetAutoRenewalWalletTransaction = "asset_auto_renewal_wallet_transaction"
// AuditResourceRoleAssetAutoRenewalOrder 表示本次自动续费产生的续费订单。
AuditResourceRoleAssetAutoRenewalOrder = "asset_auto_renewal_order"
// AuditResourceRoleRetentionMonth 表示留存清理目标自然月。
AuditResourceRoleRetentionMonth = "retention_month"
// AuditResourceRoleNotificationTarget 表示本次写操作的通知资源。

View File

@@ -157,12 +157,14 @@ const (
CardObservationSceneBusinessResume = "business_resume" // 业务复机成功
// CardObservationSceneCarrierThresholdRecovery 是通道阈值恢复扫描的只读状态查询场景。
CardObservationSceneCarrierThresholdRecovery = "carrier_threshold_recovery"
CardObservationScenePackageChanged = "package_changed" // 购包或套餐激活成功
CardObservationSceneDeviceSwitchCard = "device_switch_card" // 设备切卡成功
CardObservationSceneDeviceSwitchMode = "device_switch_mode" // 设备切卡模式成功
CardObservationSceneDeviceReboot = "device_reboot" // 设备重启成功
CardObservationSceneDeviceReset = "device_reset" // 设备恢复出厂成功
CardObservationSceneDeviceSetWiFi = "device_set_wifi" // 设备 WiFi 设置成功
// CardObservationSceneAssetAutoRenewalResumeRecovery 是资产钱包自动续费复机结果恢复扫描的只读状态查询场景。
CardObservationSceneAssetAutoRenewalResumeRecovery = "asset_auto_renewal_resume_recovery"
CardObservationScenePackageChanged = "package_changed" // 购包或套餐激活成功
CardObservationSceneDeviceSwitchCard = "device_switch_card" // 设备切卡成功
CardObservationSceneDeviceSwitchMode = "device_switch_mode" // 设备切卡模式成功
CardObservationSceneDeviceReboot = "device_reboot" // 设备重启成功
CardObservationSceneDeviceReset = "device_reset" // 设备恢复出厂成功
CardObservationSceneDeviceSetWiFi = "device_set_wifi" // 设备 WiFi 设置成功
)
// 卡实名状态变更 Outbox 事件

View File

@@ -101,6 +101,10 @@ const (
// 运营商通道流量阈值任务类型(由 Asynq Scheduler 调度)
TaskTypeCarrierThresholdCycle = "carrier_threshold:cycle" // 通道阈值周期处理:跨期解锁与条件复机
TaskTypeCarrierThresholdRecovery = "carrier_threshold:recovery" // 通道阈值停复机结果恢复扫描
// 资产钱包自动续费任务类型(由 Asynq Scheduler 调度)
TaskTypeAssetAutoRenewalScan = "asset_auto_renewal:scan" // 每日自动续费扫描:终态收敛、候选判定与续购执行
TaskTypeAssetAutoRenewalRecovery = "asset_auto_renewal:recovery" // 自动续费复机结果恢复扫描
)
// 用户状态常量
@@ -330,6 +334,9 @@ func QueueForTaskType(taskType string) string {
case TaskTypeCarrierThresholdCycle, TaskTypeCarrierThresholdRecovery:
// 与退款恢复同队列:每分钟轻量扫描,复用既有已监听队列,不新增队列与权重。
return QueueDefault
case TaskTypeAssetAutoRenewalScan, TaskTypeAssetAutoRenewalRecovery:
// 与套餐临期扫描同队列:轻量只读扫描与收敛,复用既有已监听队列,不新增队列与权重。
return QueueDataCleanup
default:
return QueueDefault
}

View File

@@ -48,6 +48,8 @@ const (
NotificationTypeH5PopupOperation = "h5.popup.operation"
// NotificationTypePackageTrafficAlert 表示套餐真流量达量预警。
NotificationTypePackageTrafficAlert = "package.traffic.alert"
// NotificationTypeAssetAutoRenewalFailed 表示资产钱包自动续费失败(含复机失败)。
NotificationTypeAssetAutoRenewalFailed = "asset.auto_renewal.failed"
// NotificationRefTypeSystemConfig 表示系统配置资源引用。
NotificationRefTypeSystemConfig = "system_config"

View File

@@ -83,6 +83,7 @@ func BuildDocHandlers() *bootstrap.Handlers {
ShopBusinessOwnerImport: admin.NewShopBusinessOwnerImportHandler(nil),
PhoneAssetAssociation: admin.NewPhoneAssetAssociationHandler(nil, nil),
PackageTrafficAlert: admin.NewPackageTrafficAlertHandler(nil, nil, nil, nil),
AssetAutoRenewal: admin.NewAssetAutoRenewalConfigHandler(nil, nil),
ClientWechat: app.NewClientWechatHandler(nil, nil, nil),
SuperAdmin: admin.NewSuperAdminHandler(nil),
SystemConfig: admin.NewSystemConfigHandler(nil, nil),

View File

@@ -4,6 +4,7 @@ import (
"context"
agentrechargeApp "github.com/break/junhong_cmp_fiber/internal/application/agentrecharge"
assetautorenewalApp "github.com/break/junhong_cmp_fiber/internal/application/assetautorenewal"
cardObservationApp "github.com/break/junhong_cmp_fiber/internal/application/cardobservation"
carrierthresholdApp "github.com/break/junhong_cmp_fiber/internal/application/carrierthreshold"
priorityapp "github.com/break/junhong_cmp_fiber/internal/application/prioritypolling"
@@ -78,7 +79,10 @@ type WorkerServices struct {
StopResumeService packagepkg.StopResumeCallback // 停复机服务,用于注入 Scheduler
// CarrierThreshold 是运营商通道流量阈值的唯一用例实例:达量判定、持锁拒绝、停复机消费者与
// 两个计划任务共用它,使直接执行的成功与恢复确认的成功走同一回写路径。
CarrierThreshold *carrierthresholdApp.Service
CarrierThreshold *carrierthresholdApp.Service
// AssetAutoRenewal 是资产钱包自动续费的唯一用例实例:每日扫描、复机消费者与恢复扫描共用它,
// 使直接执行的成功与恢复确认的成功走同一回写路径。
AssetAutoRenewal *assetautorenewalApp.Service
OrderExpirer OrderExpirer // 订单超时取消服务(接口类型,避免循环依赖)
AssetPackageOrderCreator task.AssetPackageBatchOrderCreator // 批量订购复用后台单笔订单规则
DeviceBatchAllocator task.DeviceBatchAllocationExecutor // 设备CSV批量分配复用现有业务规则