// Package agent_recharge 提供代理预充值的业务逻辑服务 // 包含充值订单创建、线下确认、支付回调处理、列表查询等功能 package agent_recharge import ( "context" "fmt" "math/rand" "time" "github.com/redis/go-redis/v9" "go.uber.org/zap" "golang.org/x/crypto/bcrypt" "gorm.io/gorm" "github.com/break/junhong_cmp_fiber/internal/model" "github.com/break/junhong_cmp_fiber/internal/model/dto" "github.com/break/junhong_cmp_fiber/internal/store/postgres" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" "github.com/break/junhong_cmp_fiber/pkg/middleware" ) // AuditServiceInterface 审计日志服务接口 type AuditServiceInterface interface { LogOperation(ctx context.Context, log *model.AccountOperationLog) } // WechatConfigServiceInterface 支付配置服务接口 type WechatConfigServiceInterface interface { GetActiveConfig(ctx context.Context) (*model.WechatConfig, error) } // Service 代理预充值业务服务 // 负责代理钱包充值订单的创建、线下确认、回调处理等业务逻辑 type Service struct { db *gorm.DB agentRechargeStore *postgres.AgentRechargeStore agentWalletStore *postgres.AgentWalletStore agentWalletTxStore *postgres.AgentWalletTransactionStore shopStore *postgres.ShopStore accountStore *postgres.AccountStore wechatConfigService WechatConfigServiceInterface auditService AuditServiceInterface redis *redis.Client logger *zap.Logger } // New 创建代理预充值服务实例 func New( db *gorm.DB, agentRechargeStore *postgres.AgentRechargeStore, agentWalletStore *postgres.AgentWalletStore, agentWalletTxStore *postgres.AgentWalletTransactionStore, shopStore *postgres.ShopStore, accountStore *postgres.AccountStore, wechatConfigService WechatConfigServiceInterface, auditService AuditServiceInterface, rdb *redis.Client, logger *zap.Logger, ) *Service { return &Service{ db: db, agentRechargeStore: agentRechargeStore, agentWalletStore: agentWalletStore, agentWalletTxStore: agentWalletTxStore, shopStore: shopStore, accountStore: accountStore, wechatConfigService: wechatConfigService, auditService: auditService, redis: rdb, logger: logger, } } // Create 创建代理充值订单 // POST /api/admin/agent-recharges func (s *Service) Create(ctx context.Context, req *dto.CreateAgentRechargeRequest) (*dto.AgentRechargeResponse, error) { userID := middleware.GetUserIDFromContext(ctx) userType := middleware.GetUserTypeFromContext(ctx) userShopID := middleware.GetShopIDFromContext(ctx) // 代理只能充自己店铺 if userType == constants.UserTypeAgent && req.ShopID != userShopID { return nil, errors.New(errors.CodeForbidden, "代理只能为自己的店铺充值") } // 线下充值仅平台可用 if req.PaymentMethod == "offline" && userType != constants.UserTypePlatform && userType != constants.UserTypeSuperAdmin { return nil, errors.New(errors.CodeForbidden, "线下充值仅平台管理员可操作") } if req.Amount < constants.AgentRechargeMinAmount || req.Amount > constants.AgentRechargeMaxAmount { return nil, errors.New(errors.CodeInvalidParam, "充值金额超出允许范围") } // 查找目标店铺的主钱包 wallet, err := s.agentWalletStore.GetMainWallet(ctx, req.ShopID) if err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New(errors.CodeNotFound, "目标店铺主钱包不存在") } return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询店铺主钱包失败") } // 查询店铺名称 shop, err := s.shopStore.GetByID(ctx, req.ShopID) if err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New(errors.CodeNotFound, "目标店铺不存在") } return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询店铺失败") } // 在线支付需要查询生效的支付配置 var paymentConfigID *uint var paymentChannel string if req.PaymentMethod == "wechat" { activeConfig, cfgErr := s.wechatConfigService.GetActiveConfig(ctx) if cfgErr != nil || activeConfig == nil { return nil, errors.New(errors.CodeNoPaymentConfig, "当前无可用的支付配置,请联系管理员") } paymentConfigID = &activeConfig.ID paymentChannel = activeConfig.ProviderType } else { paymentChannel = "offline" } rechargeNo := s.generateRechargeNo() record := &model.AgentRechargeRecord{ UserID: userID, AgentWalletID: wallet.ID, ShopID: req.ShopID, RechargeNo: rechargeNo, Amount: req.Amount, PaymentMethod: req.PaymentMethod, PaymentChannel: &paymentChannel, PaymentConfigID: paymentConfigID, Status: constants.RechargeStatusPending, ShopIDTag: wallet.ShopIDTag, EnterpriseIDTag: wallet.EnterpriseIDTag, } if err := s.agentRechargeStore.Create(ctx, record); err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "创建充值订单失败") } s.logger.Info("创建代理充值订单成功", zap.Uint("recharge_id", record.ID), zap.String("recharge_no", rechargeNo), zap.Int64("amount", req.Amount), zap.Uint("shop_id", req.ShopID), zap.Uint("user_id", userID), ) return toResponse(record, shop.ShopName), nil } // OfflinePay 线下充值确认 // POST /api/admin/agent-recharges/:id/offline-pay func (s *Service) OfflinePay(ctx context.Context, id uint, req *dto.AgentOfflinePayRequest) (*dto.AgentRechargeResponse, error) { userID := middleware.GetUserIDFromContext(ctx) userType := middleware.GetUserTypeFromContext(ctx) // 仅平台账号可操作 if userType != constants.UserTypePlatform && userType != constants.UserTypeSuperAdmin { return nil, errors.New(errors.CodeForbidden, "仅平台管理员可确认线下充值") } // 验证操作密码 account, err := s.accountStore.GetByID(ctx, userID) if err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询操作人账号失败") } if err := bcrypt.CompareHashAndPassword([]byte(account.Password), []byte(req.OperationPassword)); err != nil { return nil, errors.New(errors.CodeInvalidParam, "操作密码错误") } record, err := s.agentRechargeStore.GetByID(ctx, id) if err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New(errors.CodeNotFound, "充值记录不存在") } return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询充值记录失败") } if record.PaymentMethod != "offline" { return nil, errors.New(errors.CodeInvalidParam, "该订单非线下充值,不支持此操作") } if record.Status != constants.RechargeStatusPending { return nil, errors.New(errors.CodeInvalidParam, "该订单状态不允许确认支付") } // 查询钱包(事务内需要用到 version) wallet, err := s.agentWalletStore.GetByID(ctx, record.AgentWalletID) if err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询代理钱包失败") } now := time.Now() err = s.db.Transaction(func(tx *gorm.DB) error { // 条件更新充值记录状态 result := tx.Model(&model.AgentRechargeRecord{}). Where("id = ? AND status = ?", record.ID, constants.RechargeStatusPending). Updates(map[string]interface{}{ "status": constants.RechargeStatusCompleted, "paid_at": now, "completed_at": now, }) if result.Error != nil { return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新充值记录状态失败") } if result.RowsAffected == 0 { return errors.New(errors.CodeInvalidParam, "充值记录状态已变更") } // 增加钱包余额(乐观锁) balanceResult := tx.Model(&model.AgentWallet{}). Where("id = ? AND version = ?", wallet.ID, wallet.Version). Updates(map[string]interface{}{ "balance": gorm.Expr("balance + ?", record.Amount), "version": gorm.Expr("version + 1"), }) if balanceResult.Error != nil { return errors.Wrap(errors.CodeDatabaseError, balanceResult.Error, "更新钱包余额失败") } if balanceResult.RowsAffected == 0 { return errors.New(errors.CodeInternalError, "钱包余额更新冲突,请重试") } // 创建钱包交易记录 remark := "线下充值确认" refType := "topup" txRecord := &model.AgentWalletTransaction{ AgentWalletID: wallet.ID, ShopID: record.ShopID, UserID: userID, TransactionType: constants.AgentTransactionTypeRecharge, Amount: record.Amount, BalanceBefore: wallet.Balance, BalanceAfter: wallet.Balance + record.Amount, Status: constants.TransactionStatusSuccess, ReferenceType: &refType, ReferenceID: &record.ID, Remark: &remark, Creator: userID, ShopIDTag: wallet.ShopIDTag, EnterpriseIDTag: wallet.EnterpriseIDTag, } if err := s.agentWalletTxStore.CreateWithTx(ctx, tx, txRecord); err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "创建钱包交易记录失败") } return nil }) if err != nil { return nil, err } // 异步记录审计日志 go s.auditService.LogOperation(ctx, &model.AccountOperationLog{ OperatorID: userID, OperatorType: userType, OperationType: "offline_recharge_confirm", OperationDesc: fmt.Sprintf("确认线下充值,充值单号: %s,金额: %d分", record.RechargeNo, record.Amount), RequestID: middleware.GetRequestIDFromContext(ctx), IPAddress: middleware.GetIPFromContext(ctx), UserAgent: middleware.GetUserAgentFromContext(ctx), }) shop, _ := s.shopStore.GetByID(ctx, record.ShopID) shopName := "" if shop != nil { shopName = shop.ShopName } // 更新本地对象以反映最新状态 record.Status = constants.RechargeStatusCompleted record.PaidAt = &now record.CompletedAt = &now return toResponse(record, shopName), nil } // HandlePaymentCallback 处理支付回调 // 幂等处理:status != 待支付则直接返回成功 func (s *Service) HandlePaymentCallback(ctx context.Context, rechargeNo string, paymentMethod string, paymentTransactionID string) error { record, err := s.agentRechargeStore.GetByRechargeNo(ctx, rechargeNo) if err != nil { if err == gorm.ErrRecordNotFound { return errors.New(errors.CodeNotFound, "充值订单不存在") } return errors.Wrap(errors.CodeDatabaseError, err, "查询充值订单失败") } // 幂等检查 if record.Status != constants.RechargeStatusPending { s.logger.Info("代理充值订单已处理,跳过", zap.String("recharge_no", rechargeNo), zap.Int("status", record.Status), ) return nil } wallet, err := s.agentWalletStore.GetByID(ctx, record.AgentWalletID) if err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "查询代理钱包失败") } now := time.Now() err = s.db.Transaction(func(tx *gorm.DB) error { // 条件更新(WHERE status = 1) result := tx.Model(&model.AgentRechargeRecord{}). Where("id = ? AND status = ?", record.ID, constants.RechargeStatusPending). Updates(map[string]interface{}{ "status": constants.RechargeStatusCompleted, "payment_transaction_id": paymentTransactionID, "paid_at": now, "completed_at": now, }) if result.Error != nil { return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新充值记录状态失败") } if result.RowsAffected == 0 { return nil } // 增加钱包余额(乐观锁) balanceResult := tx.Model(&model.AgentWallet{}). Where("id = ? AND version = ?", wallet.ID, wallet.Version). Updates(map[string]interface{}{ "balance": gorm.Expr("balance + ?", record.Amount), "version": gorm.Expr("version + 1"), }) if balanceResult.Error != nil { return errors.Wrap(errors.CodeDatabaseError, balanceResult.Error, "更新钱包余额失败") } if balanceResult.RowsAffected == 0 { return errors.New(errors.CodeInternalError, "钱包余额更新冲突,请重试") } // 创建交易记录 remark := "在线支付充值" refType := "topup" txRecord := &model.AgentWalletTransaction{ AgentWalletID: wallet.ID, ShopID: record.ShopID, UserID: record.UserID, TransactionType: constants.AgentTransactionTypeRecharge, Amount: record.Amount, BalanceBefore: wallet.Balance, BalanceAfter: wallet.Balance + record.Amount, Status: constants.TransactionStatusSuccess, ReferenceType: &refType, ReferenceID: &record.ID, Remark: &remark, Creator: record.UserID, ShopIDTag: wallet.ShopIDTag, EnterpriseIDTag: wallet.EnterpriseIDTag, } if err := s.agentWalletTxStore.CreateWithTx(ctx, tx, txRecord); err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "创建钱包交易记录失败") } return nil }) if err != nil { return err } s.logger.Info("代理充值支付回调处理成功", zap.String("recharge_no", rechargeNo), zap.Int64("amount", record.Amount), zap.Uint("shop_id", record.ShopID), ) return nil } // GetByID 根据ID查询充值订单详情 // GET /api/admin/agent-recharges/:id func (s *Service) GetByID(ctx context.Context, id uint) (*dto.AgentRechargeResponse, error) { record, err := s.agentRechargeStore.GetByID(ctx, id) if err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New(errors.CodeNotFound, "充值记录不存在") } return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询充值记录失败") } shop, _ := s.shopStore.GetByID(ctx, record.ShopID) shopName := "" if shop != nil { shopName = shop.ShopName } return toResponse(record, shopName), nil } // List 分页查询充值订单列表 // GET /api/admin/agent-recharges func (s *Service) List(ctx context.Context, req *dto.AgentRechargeListRequest) ([]*dto.AgentRechargeResponse, int64, error) { page := req.Page pageSize := req.PageSize if page == 0 { page = 1 } if pageSize == 0 { pageSize = constants.DefaultPageSize } query := s.db.WithContext(ctx).Model(&model.AgentRechargeRecord{}) if req.ShopID != nil { query = query.Where("shop_id = ?", *req.ShopID) } if req.Status != nil { query = query.Where("status = ?", *req.Status) } if req.StartDate != "" { query = query.Where("created_at >= ?", req.StartDate+" 00:00:00") } if req.EndDate != "" { query = query.Where("created_at <= ?", req.EndDate+" 23:59:59") } var total int64 if err := query.Count(&total).Error; err != nil { return nil, 0, errors.Wrap(errors.CodeDatabaseError, err, "查询充值记录总数失败") } var records []*model.AgentRechargeRecord offset := (page - 1) * pageSize if err := query.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&records).Error; err != nil { return nil, 0, errors.Wrap(errors.CodeDatabaseError, err, "查询充值记录列表失败") } // 批量查询店铺名称 shopIDs := make([]uint, 0, len(records)) for _, r := range records { shopIDs = append(shopIDs, r.ShopID) } shopMap := make(map[uint]string) if len(shopIDs) > 0 { shops, err := s.shopStore.GetByIDs(ctx, shopIDs) if err == nil { for _, sh := range shops { shopMap[sh.ID] = sh.ShopName } } } list := make([]*dto.AgentRechargeResponse, 0, len(records)) for _, r := range records { list = append(list, toResponse(r, shopMap[r.ShopID])) } return list, total, nil } // generateRechargeNo 生成代理充值订单号 // 格式: ARCH + 14位时间戳 + 6位随机数 func (s *Service) generateRechargeNo() string { timestamp := time.Now().Format("20060102150405") randomNum := rand.Intn(1000000) return fmt.Sprintf("%s%s%06d", constants.AgentRechargeOrderPrefix, timestamp, randomNum) } // toResponse 将模型转换为响应 DTO func toResponse(record *model.AgentRechargeRecord, shopName string) *dto.AgentRechargeResponse { resp := &dto.AgentRechargeResponse{ ID: record.ID, RechargeNo: record.RechargeNo, ShopID: record.ShopID, ShopName: shopName, AgentWalletID: record.AgentWalletID, Amount: record.Amount, PaymentMethod: record.PaymentMethod, Status: record.Status, CreatedAt: record.CreatedAt.Format("2006-01-02 15:04:05"), UpdatedAt: record.UpdatedAt.Format("2006-01-02 15:04:05"), } if record.PaymentChannel != nil { resp.PaymentChannel = *record.PaymentChannel } if record.PaymentConfigID != nil { resp.PaymentConfigID = record.PaymentConfigID } if record.PaymentTransactionID != nil { resp.PaymentTransactionID = *record.PaymentTransactionID } if record.PaidAt != nil { t := record.PaidAt.Format("2006-01-02 15:04:05") resp.PaidAt = &t } if record.CompletedAt != nil { t := record.CompletedAt.Format("2006-01-02 15:04:05") resp.CompletedAt = &t } return resp }