package audit import ( "context" "strconv" "gorm.io/gorm" "github.com/break/junhong_cmp_fiber/internal/model" "github.com/break/junhong_cmp_fiber/pkg/auditcontext" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" ) // WriteRefundChannelResult 在调用方事务内写入渠道原路退款的调用、恢复与异常事实。 // // 主资源固定为退款单,渠道原路退款事实作为引用资源;快照只记录业务标识、金额、状态与 // 结构化失败分类,不记录商户密钥、渠道报文或客户收款信息原文。摘要由调用方提供, // 必须为不含凭证与渠道报文的简短中文说明。 // // 渠道调用与恢复都由后台任务触发,因此操作者固定为系统任务;审计上下文提供了更具体的 // 操作者标识时沿用,避免任务未装配审计上下文时审计被拒绝并连带回滚业务事务。 func (w *Writer) WriteRefundChannelResult(ctx context.Context, tx *gorm.DB, refund *model.RefundRequest, action, message string) error { if refund == nil || refund.ID == 0 || refund.RefundNo == "" { return errors.New(errors.CodeInvalidParam, "退款渠道审计资源不完整") } summary := message if summary == "" { summary = "渠道原路退款结果更新" } refundID := strconv.FormatUint(uint64(refund.ID), 10) actorID, actorName := constants.AuditActorIDRefundChannel, "退款渠道原路退款任务" if linkage := auditcontext.From(ctx); linkage.ActorID != "" { actorID = linkage.ActorID if linkage.ActorName != "" { actorName = linkage.ActorName } } resources := refundChannelResources(refund, refundID, summary) // 用 AppendAndGet 而非 Append:渠道调用与恢复的审计属于「要求成功必达」的事实, // 必须与业务事实同事务原子提交,失败时向调用方显式返回错误。 if _, err := w.AppendAndGet(ctx, tx, AppendInput{ ActionCode: action, Summary: summary, Actor: ActorInput{Kind: constants.AuditActorSystemTask, ID: actorID, Name: actorName}, Source: constants.AuditSourceWorker, ScopeType: constants.AuditScopePlatform, Result: constants.AuditResultSuccess, CorrelationID: refund.RefundNo, Resources: resources, }); err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "写入退款渠道审计失败") } return nil } // refundChannelResources 构造退款渠道审计资源:退款单为主资源,渠道原路退款事实为引用资源。 func refundChannelResources(refund *model.RefundRequest, refundID, summary string) []ResourceInput { return []ResourceInput{ { Type: constants.AuditResourceRefund, ID: &refundID, Key: refund.RefundNo, DisplayName: refund.RefundNo, Relation: constants.AuditResourceRelationPrimary, Role: constants.AuditResourceRoleRefundTarget, IdentitySnapshot: map[string]any{ "id": refund.ID, "refund_no": refund.RefundNo, "method": refund.Method, "frozen_actual_received_amount": refund.FrozenActualReceivedAmount, "requested_refund_amount": refund.RequestedRefundAmount, "approved_refund_amount": refund.ApprovedRefundAmount, "status": refund.Status, "channel_refund_status": refund.ChannelRefundStatus, "channel_refund_no": refund.ChannelRefundNo, "channel_refund_request_no": refund.ChannelRefundRequestNo, "channel_refund_amount": refund.ChannelRefundAmount, "failure_reason": refund.FailureReason, "anomaly_flag": refund.AnomalyFlag, }, AfterData: channelRefundState(refund), SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: summary, }, { Type: constants.AuditResourceRefundChannelRefund, ID: &refundID, Key: refund.RefundNo, DisplayName: "渠道原路退款 " + refund.RefundNo, Relation: constants.AuditResourceRelationReference, Role: constants.AuditResourceRoleRefundChannelRefund, IdentitySnapshot: map[string]any{ "id": refund.ID, "refund_no": refund.RefundNo, "channel_refund_status": refund.ChannelRefundStatus, "channel_refund_no": refund.ChannelRefundNo, "channel_refund_request_no": refund.ChannelRefundRequestNo, "channel_refund_amount": refund.ChannelRefundAmount, "failure_reason": refund.FailureReason, "anomaly_flag": refund.AnomalyFlag, }, }, } } // channelRefundState 构造渠道原路退款状态快照,只包含状态与结构化失败分类。 func channelRefundState(refund *model.RefundRequest) map[string]any { state := map[string]any{ "status": refund.Status, "channel_refund_status": refund.ChannelRefundStatus, "channel_refund_amount": refund.ChannelRefundAmount, "failure_reason": refund.FailureReason, "anomaly_flag": refund.AnomalyFlag, } if refund.ChannelRefundedAt != nil { state["channel_refunded_at"] = *refund.ChannelRefundedAt } return state }