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:
74
internal/infrastructure/audit/asset_auto_renewal.go
Normal file
74
internal/infrastructure/audit/asset_auto_renewal.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package audit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
pkgerrors "github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||||
)
|
||||
|
||||
// AssetAutoRenewalConfigAudit 是资产钱包自动续费配置保存的审计事实。
|
||||
//
|
||||
// 前后值快照由调用方在保存事务内组装:Before 为保存前的开关、范围、集合、天数与版本,
|
||||
// After 为保存后的对应值。审计写入与配置保存同事务,未注册时整体失败(fail-closed)。
|
||||
type AssetAutoRenewalConfigAudit struct {
|
||||
OperatorID uint
|
||||
OperationType string
|
||||
Description string
|
||||
BeforeData map[string]any
|
||||
AfterData map[string]any
|
||||
Result string
|
||||
ErrorCode string
|
||||
ErrorSummary string
|
||||
RequestID string
|
||||
CorrelationID string
|
||||
}
|
||||
|
||||
// WriteAssetAutoRenewalConfigChange 将自动续费配置变化转换为统一 Audit Event。
|
||||
func (w *Writer) WriteAssetAutoRenewalConfigChange(ctx context.Context, tx *gorm.DB, change AssetAutoRenewalConfigAudit) error {
|
||||
operationType := change.OperationType
|
||||
if operationType != "" && operationType != constants.AuditOperationAssetAutoRenewalConfigUpdate {
|
||||
return pkgerrors.New(pkgerrors.CodeInvalidParam, "审计动作未注册")
|
||||
}
|
||||
action, ok := w.registry.ActionByOperation(constants.AuditOperationAssetAutoRenewalConfigUpdate)
|
||||
if !ok {
|
||||
return pkgerrors.New(pkgerrors.CodeInvalidParam, "审计动作未注册")
|
||||
}
|
||||
if change.OperatorID == 0 {
|
||||
return pkgerrors.New(pkgerrors.CodeInvalidParam, "自动续费配置审计操作者缺失")
|
||||
}
|
||||
result := change.Result
|
||||
if result == "" {
|
||||
result = constants.AuditResultSuccess
|
||||
}
|
||||
summary := change.Description
|
||||
if summary == "" {
|
||||
summary = "保存资产钱包自动续费配置"
|
||||
}
|
||||
configID := strconv.FormatUint(uint64(constants.AssetAutoRenewalConfigSingletonID), 10)
|
||||
return w.Append(ctx, tx, AppendInput{
|
||||
ActionCode: action.Code, Summary: summary,
|
||||
Actor: ActorInput{
|
||||
Kind: constants.AuditActorAccount, ID: strconv.FormatUint(uint64(change.OperatorID), 10),
|
||||
Name: middleware.GetUsernameFromContext(ctx),
|
||||
},
|
||||
Source: action.Source, RequestPath: contextString(middleware.GetRequestPathFromContext(ctx)),
|
||||
RequestMethod: contextString(middleware.GetRequestMethodFromContext(ctx)),
|
||||
IPAddress: contextString(middleware.GetIPFromContext(ctx)), UserAgent: contextString(middleware.GetUserAgentFromContext(ctx)),
|
||||
ScopeType: constants.AuditScopePlatform, Result: result,
|
||||
ErrorCode: change.ErrorCode, ErrorSummary: change.ErrorSummary,
|
||||
RequestID: change.RequestID, CorrelationID: change.CorrelationID,
|
||||
Resources: []ResourceInput{{
|
||||
Type: constants.AuditResourceAssetAutoRenewalConfig, ID: &configID, Key: configID,
|
||||
DisplayName: constants.AssetAutoRenewalConfigDisplayName,
|
||||
Relation: constants.AuditResourceRelationPrimary,
|
||||
Role: constants.AuditResourceRoleAssetAutoRenewalConfigTarget,
|
||||
BeforeData: change.BeforeData, AfterData: change.AfterData,
|
||||
SubjectVisibility: constants.AuditSubjectInternalOnly,
|
||||
}},
|
||||
})
|
||||
}
|
||||
@@ -436,9 +436,18 @@ func NewRegistry() *Registry {
|
||||
constants.AuditActionCommissionWithdrawalAttemptRejected, "提现提交被拒绝", constants.AuditResourceShop)
|
||||
qualificationSubmitRejected := distributionAction(
|
||||
constants.AuditActionWithdrawalQualificationSubmitRejected, "资格提交被拒绝", constants.AuditResourceShop)
|
||||
// 资产钱包自动续费:配置保存是运营账号行为,续费与失败是系统任务行为,三者主资源不同。
|
||||
assetAutoRenewalConfigUpdated := packageConfigAction(
|
||||
constants.AuditActionAssetAutoRenewalConfigUpdated, "保存资产钱包自动续费配置",
|
||||
constants.AuditResourceAssetAutoRenewalConfig, constants.AuditRiskHigh)
|
||||
assetAutoRenewalRenewed := assetAutoRenewalAction(
|
||||
constants.AuditActionAssetAutoRenewalRenewed, "资产钱包自动续费完成", constants.AuditRiskHigh)
|
||||
assetAutoRenewalFailed := assetAutoRenewalAction(
|
||||
constants.AuditActionAssetAutoRenewalFailed, "资产钱包自动续费失败或跳过", constants.AuditRiskNormal)
|
||||
return &Registry{
|
||||
actionsByOperation: map[string]ActionDefinition{
|
||||
constants.AuditOperationSystemConfigUpdate: systemConfigUpdated,
|
||||
constants.AuditOperationAssetAutoRenewalConfigUpdate: assetAutoRenewalConfigUpdated,
|
||||
constants.AuditOperationPaymentConfigCreate: paymentConfigCreated,
|
||||
constants.AuditOperationPaymentConfigUpdate: paymentConfigUpdated,
|
||||
constants.AuditOperationPaymentConfigDelete: paymentConfigDeleted,
|
||||
@@ -748,6 +757,9 @@ func NewRegistry() *Registry {
|
||||
constants.AuditActionWithdrawalQualificationInvalidated: qualificationInvalidated,
|
||||
constants.AuditActionCommissionWithdrawalAttemptRejected: withdrawalAttemptRejected,
|
||||
constants.AuditActionWithdrawalQualificationSubmitRejected: qualificationSubmitRejected,
|
||||
constants.AuditActionAssetAutoRenewalConfigUpdated: assetAutoRenewalConfigUpdated,
|
||||
constants.AuditActionAssetAutoRenewalRenewed: assetAutoRenewalRenewed,
|
||||
constants.AuditActionAssetAutoRenewalFailed: assetAutoRenewalFailed,
|
||||
},
|
||||
resources: map[string]ResourceDefinition{
|
||||
constants.AuditResourceAccount: {
|
||||
@@ -822,6 +834,19 @@ func NewRegistry() *Registry {
|
||||
Type: constants.AuditResourceLogArchiveMonth, Name: "日志归档自然月",
|
||||
IdentityFields: []string{"month", "timezone", "range_start", "range_end"},
|
||||
},
|
||||
constants.AuditResourceAssetAutoRenewalConfig: {
|
||||
Type: constants.AuditResourceAssetAutoRenewalConfig, Name: "资产钱包自动续费配置",
|
||||
IdentityFields: []string{"id", "enabled", "scope", "package_ids", "days_before_expiry", "config_version"},
|
||||
},
|
||||
constants.AuditResourceAssetAutoRenewalAttempt: {
|
||||
Type: constants.AuditResourceAssetAutoRenewalAttempt, Name: "资产钱包自动续费尝试",
|
||||
IdentityFields: []string{
|
||||
"id", "asset_type", "asset_id", "trigger_date", "status", "failure_reason", "skip_reason",
|
||||
"current_usage_id", "current_package_id", "renew_package_id", "renew_price",
|
||||
"wallet_id", "wallet_transaction_id", "deduct_amount", "balance_before", "balance_after",
|
||||
"order_id", "order_no", "resume_status", "resume_integration_id", "attempt_seq",
|
||||
},
|
||||
},
|
||||
constants.AuditResourceDeviceBatchTask: {
|
||||
Type: constants.AuditResourceDeviceBatchTask, Name: "设备批量分配任务",
|
||||
IdentityFields: []string{"task_no", "operation_type"},
|
||||
@@ -1168,6 +1193,23 @@ func customerAssetAdminAction(code, name string) ActionDefinition {
|
||||
}
|
||||
}
|
||||
|
||||
// assetAutoRenewalAction 是资产钱包自动续费系统动作的注册模板。
|
||||
//
|
||||
// 续费与失败都由计划任务或 Worker 系统任务产生,主资源是本表新登记的自动续费尝试记录,
|
||||
// 因此不设 AllowedActor/Source 单一来源,改用 AllowedOrigins 同时接受两个系统入口。
|
||||
func assetAutoRenewalAction(code, name, risk string) ActionDefinition {
|
||||
return ActionDefinition{
|
||||
Code: code, Name: name, Category: constants.AuditCategoryAsset, Risk: risk,
|
||||
PrimaryResource: constants.AuditResourceAssetAutoRenewalAttempt, RequireTransaction: true,
|
||||
DefaultVisibility: constants.AuditSubjectInternalOnly,
|
||||
AllowedVisibility: []string{constants.AuditSubjectInternalOnly},
|
||||
AllowedOrigins: []ActionOrigin{
|
||||
{Actor: constants.AuditActorScheduledJob, Source: constants.AuditSourceScheduler},
|
||||
{Actor: constants.AuditActorSystemTask, Source: constants.AuditSourceWorker},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func packageConfigAction(code, name, primaryResource, risk string) ActionDefinition {
|
||||
return ActionDefinition{
|
||||
Code: code, Name: name, Category: constants.AuditCategoryConfiguration, Risk: risk,
|
||||
|
||||
Reference in New Issue
Block a user