feat(资产钱包自动续费): 新增全局配置、每日扫描续购与可靠复机
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 10m15s
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:
56
internal/infrastructure/assetautorenewal/commander.go
Normal file
56
internal/infrastructure/assetautorenewal/commander.go
Normal file
@@ -0,0 +1,56 @@
|
||||
// Package assetautorenewal 提供资产钱包自动续费的停复机执行适配与异步任务入口。
|
||||
package assetautorenewal
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
assetAutoRenewalApp "github.com/break/junhong_cmp_fiber/internal/application/assetautorenewal"
|
||||
iotCardSvc "github.com/break/junhong_cmp_fiber/internal/service/iot_card"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
)
|
||||
|
||||
// CardCommander 把自动续费复机端口转发到既有停复机单一事实源。
|
||||
//
|
||||
// 它只做类型与结果分类的转换:判定规则、重试、Integration Log、统一审计与观测序列全部由
|
||||
// internal/service/iot_card 提供,本适配器不复制任何规则,也不直接调用运营商接口。
|
||||
type CardCommander struct {
|
||||
service *iotCardSvc.StopResumeService
|
||||
}
|
||||
|
||||
// NewCardCommander 创建自动续费复机执行适配器。
|
||||
func NewCardCommander(service *iotCardSvc.StopResumeService) *CardCommander {
|
||||
return &CardCommander{service: service}
|
||||
}
|
||||
|
||||
// AutoRenewalResumeReady 判断该资产是否满足自动复机条件。
|
||||
func (c *CardCommander) AutoRenewalResumeReady(ctx context.Context, assetType string, assetID uint) (bool, string, error) {
|
||||
if c == nil || c.service == nil {
|
||||
return false, "", errCommanderUnavailable()
|
||||
}
|
||||
return c.service.AutoRenewalResumeReady(ctx, assetType, assetID)
|
||||
}
|
||||
|
||||
// ResumeAssetForAutoRenewal 执行复机并把结果转换为本用例的结果摘要。
|
||||
func (c *CardCommander) ResumeAssetForAutoRenewal(ctx context.Context, assetType string, assetID uint) (assetAutoRenewalApp.ResumeOutcome, error) {
|
||||
if c == nil || c.service == nil {
|
||||
return assetAutoRenewalApp.ResumeOutcome{}, errCommanderUnavailable()
|
||||
}
|
||||
outcome, err := c.service.ResumeAssetForAutoRenewal(ctx, assetType, assetID)
|
||||
return assetAutoRenewalApp.ResumeOutcome{
|
||||
Applied: outcome.Applied, IntegrationID: outcome.IntegrationID,
|
||||
Result: outcome.Result, SafeReason: outcome.SafeReason,
|
||||
}, err
|
||||
}
|
||||
|
||||
// QueryAutoRenewalResumeState 只查询运营商卡状态,供恢复扫描回填复机结果。
|
||||
func (c *CardCommander) QueryAutoRenewalResumeState(ctx context.Context, assetType string, assetID uint) (bool, bool, string, error) {
|
||||
if c == nil || c.service == nil {
|
||||
return false, false, "", errCommanderUnavailable()
|
||||
}
|
||||
return c.service.QueryAutoRenewalResumeState(ctx, assetType, assetID)
|
||||
}
|
||||
|
||||
// errCommanderUnavailable 返回复机执行端口未配置的稳定错误。
|
||||
func errCommanderUnavailable() error {
|
||||
return errors.New(errors.CodeServiceUnavailable, "自动续费复机执行能力未配置")
|
||||
}
|
||||
70
internal/infrastructure/assetautorenewal/task.go
Normal file
70
internal/infrastructure/assetautorenewal/task.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user