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,70 @@
package assetautorenewal
import (
"context"
"github.com/hibiken/asynq"
assetAutoRenewalApp "github.com/break/junhong_cmp_fiber/internal/application/assetautorenewal"
"github.com/break/junhong_cmp_fiber/pkg/auditcontext"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
)
// DailyScanTaskHandler 执行资产钱包自动续费的每日扫描任务。
//
// 审计上下文固定为计划任务来源,使终态收敛、续费成功与失败事实都可按计划任务维度追溯。
type DailyScanTaskHandler struct {
service *assetAutoRenewalApp.Service
}
// NewDailyScanTaskHandler 创建自动续费每日扫描任务处理器。
func NewDailyScanTaskHandler(service *assetAutoRenewalApp.Service) *DailyScanTaskHandler {
return &DailyScanTaskHandler{service: service}
}
// Handle 执行一次每日扫描;只有扫描级失败才返回错误交既有任务重试。
func (h *DailyScanTaskHandler) Handle(ctx context.Context, task *asynq.Task) error {
if h == nil || h.service == nil {
return errors.New(errors.CodeServiceUnavailable, "自动续费扫描任务未配置")
}
taskType := constants.TaskTypeAssetAutoRenewalScan
if task != nil && task.Type() != "" {
taskType = task.Type()
}
ctx = auditcontext.With(ctx, auditcontext.Context{
ActorKind: constants.AuditActorScheduledJob, ActorID: taskType,
ActorName: "资产钱包自动续费每日扫描计划任务", Source: constants.AuditSourceScheduler,
})
_, err := h.service.RunDailyScan(ctx)
return err
}
// RecoveryTaskHandler 执行资产钱包自动续费的复机结果恢复扫描任务。
//
// 只查询运营商状态回填,绝不重复发起复机调用;只有最终确认失败才按通知契约投递通知。
type RecoveryTaskHandler struct {
service *assetAutoRenewalApp.Service
}
// NewRecoveryTaskHandler 创建自动续费复机结果恢复任务处理器。
func NewRecoveryTaskHandler(service *assetAutoRenewalApp.Service) *RecoveryTaskHandler {
return &RecoveryTaskHandler{service: service}
}
// Handle 扫描未收敛的复机子结果并只查询状态回填。
func (h *RecoveryTaskHandler) Handle(ctx context.Context, task *asynq.Task) error {
if h == nil || h.service == nil {
return errors.New(errors.CodeServiceUnavailable, "自动续费复机结果恢复任务未配置")
}
taskType := constants.TaskTypeAssetAutoRenewalRecovery
if task != nil && task.Type() != "" {
taskType = task.Type()
}
ctx = auditcontext.With(ctx, auditcontext.Context{
ActorKind: constants.AuditActorScheduledJob, ActorID: taskType,
ActorName: "资产钱包自动续费结果恢复计划任务", Source: constants.AuditSourceScheduler,
})
_, err := h.service.RecoverResumeResults(ctx)
return err
}