Files
junhong_cmp_fiber/internal/infrastructure/audit/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

75 lines
3.0 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 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,
}},
})
}