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,零外部渠道调用)
75 lines
3.0 KiB
Go
75 lines
3.0 KiB
Go
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,
|
||
}},
|
||
})
|
||
}
|