// Package distributionwithdrawal 收口代理分销注册、提现资料资格与提现企业微信终审的用例。 // 三者都以审批尝试/资料版本/注册记录主键作为通用审批业务标识,终态消费幂等且可重放。 package distributionwithdrawal import ( "context" stderrors "errors" "strconv" "github.com/bytedance/sonic" "gorm.io/gorm" approvalapp "github.com/break/junhong_cmp_fiber/internal/application/approval" "github.com/break/junhong_cmp_fiber/internal/model" "github.com/break/junhong_cmp_fiber/pkg/auditfailure" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" ) // VerificationCodeVerifier 是公开扫码注册复用的短信验证码校验接缝。 // 校验成功即消费验证码,同一验证码不可二次使用。 type VerificationCodeVerifier interface { VerifyCode(ctx context.Context, phone string, code string) error } // AuditChange 描述分销注册、提现资格与提现审批事实的实际变化。 // 日志与审计不得记录密码、完整证件号、完整手机号或附件内容。 type AuditChange struct { // EventID 是审计事件稳定标识,同一业务事实重复重放时保持相同值。 EventID string // ActionCode 是已注册的审计动作码。 ActionCode string // Summary 是给人工阅读的中文摘要。 Summary string // CorrelationID 是来源业务链路标识。 CorrelationID string // Registration 是本次动作后的扫码注册记录事实。 Registration *model.AgentDistributionRegistration // ParentShop 是扫码注册使用的上级店铺。 ParentShop *model.Shop // Shop 是本次动作所属或引用的店铺。 Shop *model.Shop // CreatedShop 是注册审批通过时新建的店铺。 // CreatedShop.DistributionCode 是本次为新店铺生成的随机码; // AppliedDistributionCode 是注册时使用的上级店铺码快照,二者必须区分,不得混用。 CreatedShop *model.Shop // AppliedDistributionCode 是注册提交时使用的上级店铺分销码快照。 AppliedDistributionCode string // Qualification 是本次动作后的提现资料资格版本。 Qualification *model.WithdrawalQualification // Withdrawal 是本次动作后的提现申请事实。 Withdrawal *model.CommissionWithdrawalRequest // Attempt 是本次动作对应的提现审批尝试记录。 Attempt *model.CommissionWithdrawalRequestAttempt // Wallet 是本次动作影响的佣金钱包。 Wallet *model.AgentWallet // Transaction 是本次动作产生的钱包流水。 Transaction *model.AgentWalletTransaction // BeforeData 与 AfterData 是脱敏前后的字段快照。 BeforeData map[string]any AfterData map[string]any // Result 是审计结果,空值按成功处理。 Result string // ErrorCode 与 ErrorSummary 是失败或拒绝审计的稳定错误信息。 ErrorCode string ErrorSummary string } // AuditWriter 在业务事务内追加统一 Audit Event。 type AuditWriter interface { WriteDistributionWithdrawal(ctx context.Context, tx *gorm.DB, change AuditChange) error } // RecordFailure 在业务回滚后使用独立短事务记录失败或拒绝事实。 func RecordFailure(ctx context.Context, db *gorm.DB, writer AuditWriter, change AuditChange, businessErr error) { if writer == nil || db == nil || businessErr == nil { return } appErr := changeError(businessErr) change.Result = constants.AuditResultFailed switch appErr.Code { case errors.CodeForbidden, errors.CodeNotFound, errors.CodeInvalidParam, errors.CodeConflict, errors.CodeInvalidStatus, errors.CodeInsufficientBalance, errors.CodeShopLevelExceeded: change.Result = constants.AuditResultDenied } if change.ErrorCode == "" { change.ErrorCode = strconv.Itoa(appErr.Code) } if change.ErrorSummary == "" { change.ErrorSummary = appErr.Message } if err := db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { return writer.WriteDistributionWithdrawal(ctx, tx, change) }); err != nil { auditfailure.RecordSecondaryWriteFailure( change.ActionCode, "", "", change.CorrelationID, change.ErrorCode, err, ) } } // changeError 归一化底层错误为稳定 AppError,避免失败审计泄露底层文本。 func changeError(err error) *errors.AppError { var appErr *errors.AppError if stderrors.As(err, &appErr) { return appErr } return errors.New(errors.CodeInternalError, "分销注册或提现审批操作失败") } // approvalSnapshots 生成通用审批的提交人快照与业务表单快照。 func approvalSnapshots(accountID uint, accountName string, business map[string]any) ([]byte, []byte, error) { submitter, err := marshalJSON(map[string]any{ "account_id": accountID, "account_name": accountName, }) if err != nil { return nil, nil, err } request, err := marshalJSON(business) if err != nil { return nil, nil, err } return submitter, request, nil } // marshalJSON 使用 sonic 序列化业务快照,禁止写入密码、完整证件号或附件内容。 func marshalJSON(value any) ([]byte, error) { payload, err := sonic.Marshal(value) if err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "序列化审批业务快照失败") } return payload, nil } // createApprovalInTx 在业务事务内创建通用审批实例并返回引用。 func createApprovalInTx( ctx context.Context, tx *gorm.DB, port approvalapp.Port, preparation approvalapp.Preparation, businessType string, businessID uint, submitterAccountID uint, submitterSnapshot []byte, requestSnapshot []byte, correlationID string, ) (approvalapp.Reference, error) { if port == nil { return approvalapp.Reference{}, errors.New(errors.CodeServiceUnavailable, "审批能力尚未配置") } return port.CreateInTx(ctx, tx, approvalapp.CreateRequest{ Preparation: preparation, BusinessType: businessType, BusinessID: businessID, SubmitterAccountID: submitterAccountID, SubmitterSnapshot: submitterSnapshot, RequestSnapshot: requestSnapshot, CorrelationID: correlationID, }) }