package agentrecharge import ( "context" cryptorand "crypto/rand" stderrors "errors" "fmt" "math/big" "strings" "time" "gorm.io/gorm" domain "github.com/break/junhong_cmp_fiber/internal/domain/agentrecharge" "github.com/break/junhong_cmp_fiber/internal/model" "github.com/break/junhong_cmp_fiber/pkg/constants" apperrors "github.com/break/junhong_cmp_fiber/pkg/errors" "github.com/break/junhong_cmp_fiber/pkg/idempotency" ) const onlineCreationOperation = "agent-recharge.create-online" // CreateOnlineCommand 描述从认证上下文构造的在线充值命令。 type CreateOnlineCommand struct { AccountID uint UserType int CurrentShopID uint Amount int64 PaymentMethod string RequestID string PayerClientIP string } // CreateOnlineResult 返回在线充值单、支付单及支付链接。 type CreateOnlineResult struct { Recharge *model.AgentRechargeRecord Payment *model.Payment } // AvailablePaymentMethodsResult 返回当前可用渠道及在线金额边界。 type AvailablePaymentMethodsResult struct { Methods []string MinAmount int64 MaxAmount int64 } // OnlineCreationService 创建代理在线扫码充值单。 type OnlineCreationService struct { db *gorm.DB wechat OnlinePaymentPort alipay OnlinePaymentPort audit PaymentAuditWriter } // NewOnlineCreationService 创建代理在线充值用例并以结构体字段注入两个渠道 Adapter。 func NewOnlineCreationService(db *gorm.DB, wechat, alipay OnlinePaymentPort, audit PaymentAuditWriter) *OnlineCreationService { return &OnlineCreationService{db: db, wechat: wechat, alipay: alipay, audit: audit} } // Execute 以短事务建单,事务外生成支付链接,再条件保存链接或关闭失败订单。 func (s *OnlineCreationService) Execute(ctx context.Context, command CreateOnlineCommand) (*CreateOnlineResult, error) { if s == nil || s.db == nil || s.wechat == nil || s.alipay == nil || s.audit == nil { return nil, apperrors.New(apperrors.CodeServiceUnavailable, "代理在线充值能力未配置") } command.PaymentMethod = strings.TrimSpace(command.PaymentMethod) command.RequestID = strings.TrimSpace(command.RequestID) if err := domain.ValidateOnlineCreation(command.UserType, command.Amount, command.PaymentMethod); err != nil { return nil, err } if command.AccountID == 0 || command.CurrentShopID == 0 || len(command.RequestID) > 64 || !idempotency.ValidateScope(onlineCreationScope(command.AccountID), command.RequestID) { return nil, apperrors.New(apperrors.CodeInvalidParam) } fingerprint, err := idempotency.Fingerprint(struct { Amount int64 `json:"amount"` PaymentMethod string `json:"payment_method"` }{command.Amount, command.PaymentMethod}) if err != nil { return nil, apperrors.Wrap(apperrors.CodeInternalError, err, "生成在线充值请求指纹失败") } if replay, found, err := s.loadReplay(ctx, command, fingerprint); err != nil || found { return replay, err } account, shop, wallet, config, adapter, err := s.loadCreationFacts(ctx, command) if err != nil { return nil, err } result, err := s.createLocalFacts(ctx, command, fingerprint.Value, account, shop, wallet, config) if err != nil { if replay, found, replayErr := s.loadReplay(ctx, command, fingerprint); replayErr != nil || found { return replay, replayErr } return nil, err } paymentResult, err := adapter.CreatePaymentURL(ctx, OnlinePaymentRequest{ PaymentID: result.Payment.ID, PaymentNo: result.Payment.PaymentNo, CorrelationID: result.Payment.PaymentNo, Description: "代理主钱包充值", Amount: command.Amount, ExpireAt: *result.Payment.ExpireAt, PayerClientIP: command.PayerClientIP, Config: config, }) if err != nil { if !isUnknownPaymentResult(err) { if closeErr := s.closeFailedCreation(ctx, result); closeErr != nil { return nil, apperrors.Wrap(apperrors.CodeDatabaseError, closeErr, "支付链接生成失败且关闭本地订单失败") } } return nil, err } if strings.TrimSpace(paymentResult.QRContent) == "" { if closeErr := s.closeFailedCreation(ctx, result); closeErr != nil { return nil, closeErr } return nil, apperrors.New(apperrors.CodeServiceUnavailable, "支付渠道未返回支付链接") } update := s.db.WithContext(ctx).Model(&model.Payment{}). Where("id = ? AND status = ? AND qr_content = ''", result.Payment.ID, model.PaymentRecordStatusPending). Update("qr_content", paymentResult.QRContent) if update.Error != nil { return nil, apperrors.Wrap(apperrors.CodeDatabaseError, update.Error, "保存支付链接失败") } if update.RowsAffected != 1 { return nil, apperrors.New(apperrors.CodeConflict, "在线充值支付链接已变化") } result.Payment.QRContent = paymentResult.QRContent return result, nil } // AvailablePaymentMethods 按固定顺序返回配置完整的在线支付方式。 func (s *OnlineCreationService) AvailablePaymentMethods(ctx context.Context, userType int) (AvailablePaymentMethodsResult, error) { result := AvailablePaymentMethodsResult{ Methods: []string{}, MinAmount: constants.AgentOnlineRechargeMinAmount, MaxAmount: constants.AgentRechargeMaxAmount, } if userType != constants.UserTypeAgent { return result, apperrors.New(apperrors.CodeForbidden, "仅代理账号可以查询在线支付方式") } var config model.WechatConfig if err := s.db.WithContext(ctx).Where("is_active = ?", true).First(&config).Error; err != nil { if stderrors.Is(err, gorm.ErrRecordNotFound) { return result, nil } return result, apperrors.Wrap(apperrors.CodeDatabaseError, err, "查询生效支付配置失败") } if s.wechat.Available(&config) { result.Methods = append(result.Methods, constants.RechargeMethodWechat) } if s.alipay.Available(&config) { result.Methods = append(result.Methods, constants.RechargeMethodAlipay) } return result, nil } func (s *OnlineCreationService) loadCreationFacts( ctx context.Context, command CreateOnlineCommand, ) (*model.Account, *model.Shop, *model.AgentWallet, *model.WechatConfig, OnlinePaymentPort, error) { var account model.Account if err := s.db.WithContext(ctx).Where("id = ? AND user_type = ? AND status = ?", command.AccountID, constants.UserTypeAgent, constants.StatusEnabled).First(&account).Error; err != nil || account.ShopID == nil || *account.ShopID != command.CurrentShopID { if err != nil && !stderrors.Is(err, gorm.ErrRecordNotFound) { return nil, nil, nil, nil, nil, apperrors.Wrap(apperrors.CodeDatabaseError, err, "查询在线充值账号失败") } return nil, nil, nil, nil, nil, apperrors.New(apperrors.CodeForbidden, "当前代理账号不可为该店铺充值") } var shop model.Shop if err := s.db.WithContext(ctx).Where("id = ? AND status = ?", command.CurrentShopID, constants.StatusEnabled).First(&shop).Error; err != nil { if stderrors.Is(err, gorm.ErrRecordNotFound) { return nil, nil, nil, nil, nil, apperrors.New(apperrors.CodeForbidden, "无权限操作该资源或资源不存在") } return nil, nil, nil, nil, nil, apperrors.Wrap(apperrors.CodeDatabaseError, err, "查询当前店铺失败") } var wallet model.AgentWallet if err := s.db.WithContext(ctx).Where("shop_id = ? AND wallet_type = ? AND status = ?", command.CurrentShopID, constants.AgentWalletTypeMain, constants.AgentWalletStatusNormal).First(&wallet).Error; err != nil { if stderrors.Is(err, gorm.ErrRecordNotFound) { return nil, nil, nil, nil, nil, apperrors.New(apperrors.CodeWalletNotFound, "当前店铺主钱包不存在或不可用") } return nil, nil, nil, nil, nil, apperrors.Wrap(apperrors.CodeDatabaseError, err, "查询当前店铺主钱包失败") } var config model.WechatConfig if err := s.db.WithContext(ctx).Where("is_active = ?", true).First(&config).Error; err != nil { if stderrors.Is(err, gorm.ErrRecordNotFound) { return nil, nil, nil, nil, nil, apperrors.New(apperrors.CodeNoPaymentConfig) } return nil, nil, nil, nil, nil, apperrors.Wrap(apperrors.CodeDatabaseError, err, "查询生效支付配置失败") } adapter := s.adapter(command.PaymentMethod) if adapter == nil || !adapter.Available(&config) { return nil, nil, nil, nil, nil, apperrors.New(apperrors.CodeNoPaymentConfig) } return &account, &shop, &wallet, &config, adapter, nil } func (s *OnlineCreationService) createLocalFacts( ctx context.Context, command CreateOnlineCommand, fingerprint string, account *model.Account, shop *model.Shop, wallet *model.AgentWallet, config *model.WechatConfig, ) (*CreateOnlineResult, error) { rechargeNo, err := newBusinessNo(constants.AgentRechargeOrderPrefix, time.Now().Format("20060102150405")) if err != nil { return nil, err } paymentNo, err := newBusinessNo("PAY", fmt.Sprintf("%d", time.Now().UnixMilli())) if err != nil { return nil, err } expireMinutes := config.AliPayExpireMinutes if expireMinutes <= 0 { expireMinutes = model.DefaultAliPayExpireMinutes } expireAt := time.Now().Add(time.Duration(expireMinutes) * time.Minute) channel, requestID := command.PaymentMethod, command.RequestID record := &model.AgentRechargeRecord{ UserID: account.ID, AgentWalletID: wallet.ID, ShopID: shop.ID, RechargeNo: rechargeNo, Amount: command.Amount, PaymentMethod: command.PaymentMethod, PaymentChannel: &channel, PaymentConfigID: &config.ID, Status: constants.RechargeStatusPending, RequestID: &requestID, RequestFingerprint: &fingerprint, ShopIDTag: wallet.ShopIDTag, EnterpriseIDTag: wallet.EnterpriseIDTag, } payment := &model.Payment{ PaymentNo: paymentNo, OrderType: model.PaymentOrderTypeAgentRecharge, PaymentMethod: command.PaymentMethod, MerchantIdentity: paymentMerchantIdentity(command.PaymentMethod, config), Amount: command.Amount, Status: model.PaymentRecordStatusPending, PaymentConfigID: &config.ID, ExpireAt: &expireAt, } err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := tx.Create(record).Error; err != nil { return err } payment.OrderID = record.ID if err := tx.Create(payment).Error; err != nil { return err } return s.audit.WriteAgentRechargePayment(ctx, tx, PaymentAudit{ ActionCode: constants.AuditActionPaymentCreated, Summary: "创建代理充值支付记录", Payment: payment, Recharge: record, AfterData: map[string]any{"status": payment.Status}, }) }) if err != nil { return nil, apperrors.Wrap(apperrors.CodeDatabaseError, err, "创建在线充值本地订单失败") } return &CreateOnlineResult{Recharge: record, Payment: payment}, nil } func paymentMerchantIdentity(paymentMethod string, config *model.WechatConfig) string { if config == nil { return "" } if paymentMethod == constants.RechargeMethodWechat { return config.WxMchID } if paymentMethod == constants.RechargeMethodAlipay { return config.AliAppID } return "" } func (s *OnlineCreationService) loadReplay( ctx context.Context, command CreateOnlineCommand, fingerprint idempotency.FingerprintValue, ) (*CreateOnlineResult, bool, error) { var record model.AgentRechargeRecord err := s.db.WithContext(ctx).Where("user_id = ? AND request_id = ?", command.AccountID, command.RequestID).First(&record).Error if stderrors.Is(err, gorm.ErrRecordNotFound) { return nil, false, nil } if err != nil { return nil, false, apperrors.Wrap(apperrors.CodeDatabaseError, err, "读取在线充值幂等记录失败") } existingFingerprint := "" if record.RequestFingerprint != nil { existingFingerprint = *record.RequestFingerprint } if existingFingerprint != fingerprint.Value { return nil, true, apperrors.New(apperrors.CodeConflict, "同一请求标识对应的充值内容不一致") } var payment model.Payment if err := s.db.WithContext(ctx).Where("order_id = ? AND order_type = ?", record.ID, model.PaymentOrderTypeAgentRecharge).First(&payment).Error; err != nil { return nil, true, apperrors.Wrap(apperrors.CodeDatabaseError, err, "读取在线充值支付单失败") } if payment.QRContent == "" { if record.Status == constants.RechargeStatusClosed || payment.Status == model.PaymentRecordStatusFailed { return nil, true, apperrors.New(apperrors.CodeInvalidStatus, "原在线充值请求支付链接生成失败") } return nil, true, apperrors.New(apperrors.CodeConflict, "在线充值请求正在处理中,请稍后重试") } return &CreateOnlineResult{Recharge: &record, Payment: &payment}, true, nil } func (s *OnlineCreationService) closeFailedCreation(ctx context.Context, result *CreateOnlineResult) error { if result == nil || result.Recharge == nil || result.Payment == nil || !domain.CanCloseAfterPaymentURLFailure(result.Recharge.Status) { return nil } return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { paymentUpdate := tx.Model(&model.Payment{}). Where("id = ? AND status = ?", result.Payment.ID, model.PaymentRecordStatusPending). Update("status", model.PaymentRecordStatusFailed) if paymentUpdate.Error != nil { return apperrors.Wrap(apperrors.CodeDatabaseError, paymentUpdate.Error, "关闭失败支付单失败") } rechargeUpdate := tx.Model(&model.AgentRechargeRecord{}). Where("id = ? AND status = ?", result.Recharge.ID, constants.RechargeStatusPending). Update("status", constants.RechargeStatusClosed) if rechargeUpdate.Error != nil { return apperrors.Wrap(apperrors.CodeDatabaseError, rechargeUpdate.Error, "关闭失败充值单失败") } afterPayment := *result.Payment afterPayment.Status = model.PaymentRecordStatusFailed return s.audit.WriteAgentRechargePayment(ctx, tx, PaymentAudit{ ActionCode: constants.AuditActionPaymentFailed, Summary: "支付链接生成失败,关闭支付记录", Payment: &afterPayment, Recharge: result.Recharge, BeforeData: map[string]any{"status": result.Payment.Status}, AfterData: map[string]any{"status": afterPayment.Status}, RechargeBeforeData: map[string]any{"status": result.Recharge.Status}, RechargeAfterData: map[string]any{"status": constants.RechargeStatusClosed}, }) }) } func (s *OnlineCreationService) adapter(paymentMethod string) OnlinePaymentPort { if paymentMethod == constants.RechargeMethodWechat { return s.wechat } if paymentMethod == constants.RechargeMethodAlipay { return s.alipay } return nil } func onlineCreationScope(accountID uint) idempotency.Scope { return idempotency.Scope{Subject: fmt.Sprintf("account:%d", accountID), Operation: onlineCreationOperation} } func newBusinessNo(prefix, timestamp string) (string, error) { random, err := cryptorand.Int(cryptorand.Reader, big.NewInt(1000000)) if err != nil { return "", apperrors.Wrap(apperrors.CodeInternalError, err, "生成业务单号失败") } return fmt.Sprintf("%s%s%06d", prefix, timestamp, random.Int64()), nil } func isUnknownPaymentResult(err error) bool { var appErr *apperrors.AppError return stderrors.As(err, &appErr) && appErr.Code == apperrors.CodeTimeout }