diff --git a/internal/handler/admin/agent_recharge.go b/internal/handler/admin/agent_recharge.go new file mode 100644 index 0000000..9d28fb2 --- /dev/null +++ b/internal/handler/admin/agent_recharge.go @@ -0,0 +1,91 @@ +package admin + +import ( + "strconv" + + "github.com/gofiber/fiber/v2" + + "github.com/break/junhong_cmp_fiber/internal/model/dto" + agentRechargeSvc "github.com/break/junhong_cmp_fiber/internal/service/agent_recharge" + "github.com/break/junhong_cmp_fiber/pkg/errors" + "github.com/break/junhong_cmp_fiber/pkg/response" +) + +// AgentRechargeHandler 代理预充值 Handler +type AgentRechargeHandler struct { + service *agentRechargeSvc.Service +} + +// NewAgentRechargeHandler 创建代理预充值 Handler +func NewAgentRechargeHandler(service *agentRechargeSvc.Service) *AgentRechargeHandler { + return &AgentRechargeHandler{service: service} +} + +// Create 创建代理充值订单 +// POST /api/admin/agent-recharges +func (h *AgentRechargeHandler) Create(c *fiber.Ctx) error { + var req dto.CreateAgentRechargeRequest + if err := c.BodyParser(&req); err != nil { + return errors.New(errors.CodeInvalidParam, "请求参数解析失败") + } + + result, err := h.service.Create(c.UserContext(), &req) + if err != nil { + return err + } + + return response.Success(c, result) +} + +// List 查询代理充值订单列表 +// GET /api/admin/agent-recharges +func (h *AgentRechargeHandler) List(c *fiber.Ctx) error { + var req dto.AgentRechargeListRequest + if err := c.QueryParser(&req); err != nil { + return errors.New(errors.CodeInvalidParam, "请求参数解析失败") + } + + list, total, err := h.service.List(c.UserContext(), &req) + if err != nil { + return err + } + + return response.SuccessWithPagination(c, list, total, req.Page, req.PageSize) +} + +// Get 查询代理充值订单详情 +// GET /api/admin/agent-recharges/:id +func (h *AgentRechargeHandler) Get(c *fiber.Ctx) error { + id, err := strconv.ParseUint(c.Params("id"), 10, 64) + if err != nil { + return errors.New(errors.CodeInvalidParam, "无效的充值记录ID") + } + + result, err := h.service.GetByID(c.UserContext(), uint(id)) + if err != nil { + return err + } + + return response.Success(c, result) +} + +// OfflinePay 确认线下充值 +// POST /api/admin/agent-recharges/:id/offline-pay +func (h *AgentRechargeHandler) OfflinePay(c *fiber.Ctx) error { + id, err := strconv.ParseUint(c.Params("id"), 10, 64) + if err != nil { + return errors.New(errors.CodeInvalidParam, "无效的充值记录ID") + } + + var req dto.AgentOfflinePayRequest + if err := c.BodyParser(&req); err != nil { + return errors.New(errors.CodeInvalidParam, "请求参数解析失败") + } + + result, err := h.service.OfflinePay(c.UserContext(), uint(id), &req) + if err != nil { + return err + } + + return response.Success(c, result) +} diff --git a/internal/model/dto/agent_recharge_dto.go b/internal/model/dto/agent_recharge_dto.go new file mode 100644 index 0000000..4dc542a --- /dev/null +++ b/internal/model/dto/agent_recharge_dto.go @@ -0,0 +1,50 @@ +package dto + +// CreateAgentRechargeRequest 创建代理充值请求 +type CreateAgentRechargeRequest struct { + ShopID uint `json:"shop_id" validate:"required" required:"true" description:"目标店铺ID,代理只能填自己店铺"` + Amount int64 `json:"amount" validate:"required,min=10000,max=100000000" required:"true" minimum:"10000" maximum:"100000000" description:"充值金额(分),范围100元~100万元"` + PaymentMethod string `json:"payment_method" validate:"required,oneof=wechat offline" required:"true" description:"支付方式 (wechat:微信在线支付, offline:线下转账仅平台可用)"` +} + +// AgentOfflinePayRequest 代理线下充值确认请求 +type AgentOfflinePayRequest struct { + OperationPassword string `json:"operation_password" validate:"required" required:"true" description:"操作密码"` +} + +// AgentRechargeResponse 代理充值记录响应 +type AgentRechargeResponse struct { + ID uint `json:"id" description:"充值记录ID"` + RechargeNo string `json:"recharge_no" description:"充值单号(ARCH前缀)"` + ShopID uint `json:"shop_id" description:"店铺ID"` + ShopName string `json:"shop_name" description:"店铺名称"` + AgentWalletID uint `json:"agent_wallet_id" description:"代理钱包ID"` + Amount int64 `json:"amount" description:"充值金额(分)"` + PaymentMethod string `json:"payment_method" description:"支付方式 (wechat:微信在线支付, offline:线下转账)"` + PaymentChannel string `json:"payment_channel" description:"实际支付通道 (wechat_direct:微信直连, fuyou:富友, offline:线下转账)"` + PaymentConfigID *uint `json:"payment_config_id" description:"关联支付配置ID,线下充值为null"` + PaymentTransactionID string `json:"payment_transaction_id" description:"第三方支付流水号"` + Status int `json:"status" description:"状态 (1:待支付, 2:已完成, 3:已取消)"` + PaidAt *string `json:"paid_at" description:"支付时间"` + CompletedAt *string `json:"completed_at" description:"完成时间"` + CreatedAt string `json:"created_at" description:"创建时间"` + UpdatedAt string `json:"updated_at" description:"更新时间"` +} + +// AgentRechargeListRequest 代理充值记录列表请求 +type AgentRechargeListRequest struct { + Page int `json:"page" query:"page" validate:"omitempty,min=1" minimum:"1" description:"页码,默认1"` + PageSize int `json:"page_size" query:"page_size" validate:"omitempty,min=1,max=100" minimum:"1" maximum:"100" description:"每页条数,默认20,最大100"` + ShopID *uint `json:"shop_id" query:"shop_id" description:"按店铺ID过滤"` + Status *int `json:"status" query:"status" description:"按状态过滤 (1:待支付, 2:已完成, 3:已取消)"` + StartDate string `json:"start_date" query:"start_date" description:"创建时间起始日期(YYYY-MM-DD)"` + EndDate string `json:"end_date" query:"end_date" description:"创建时间截止日期(YYYY-MM-DD)"` +} + +// AgentRechargeListResponse 代理充值记录列表响应 +type AgentRechargeListResponse struct { + Total int64 `json:"total" description:"总记录数"` + Page int `json:"page" description:"当前页码"` + PageSize int `json:"page_size" description:"每页条数"` + List []*AgentRechargeResponse `json:"list" description:"充值记录列表"` +} diff --git a/internal/routes/agent_recharge.go b/internal/routes/agent_recharge.go new file mode 100644 index 0000000..2241405 --- /dev/null +++ b/internal/routes/agent_recharge.go @@ -0,0 +1,55 @@ +package routes + +import ( + "github.com/gofiber/fiber/v2" + + "github.com/break/junhong_cmp_fiber/internal/handler/admin" + "github.com/break/junhong_cmp_fiber/internal/model/dto" + "github.com/break/junhong_cmp_fiber/pkg/constants" + "github.com/break/junhong_cmp_fiber/pkg/errors" + "github.com/break/junhong_cmp_fiber/pkg/middleware" + "github.com/break/junhong_cmp_fiber/pkg/openapi" +) + +func registerAgentRechargeRoutes(router fiber.Router, handler *admin.AgentRechargeHandler, doc *openapi.Generator, basePath string) { + group := router.Group("/agent-recharges", func(c *fiber.Ctx) error { + userType := middleware.GetUserTypeFromContext(c.UserContext()) + if userType == constants.UserTypeEnterprise { + return errors.New(errors.CodeForbidden, "企业账号无权访问代理充值功能") + } + return c.Next() + }) + groupPath := basePath + "/agent-recharges" + + Register(group, doc, groupPath, "POST", "", handler.Create, RouteSpec{ + Summary: "创建代理充值订单", + Tags: []string{"代理预充值"}, + Input: new(dto.CreateAgentRechargeRequest), + Output: new(dto.AgentRechargeResponse), + Auth: true, + }) + + Register(group, doc, groupPath, "GET", "", handler.List, RouteSpec{ + Summary: "查询代理充值订单列表", + Tags: []string{"代理预充值"}, + Input: new(dto.AgentRechargeListRequest), + Output: new(dto.AgentRechargeListResponse), + Auth: true, + }) + + Register(group, doc, groupPath, "GET", "/:id", handler.Get, RouteSpec{ + Summary: "查询代理充值订单详情", + Tags: []string{"代理预充值"}, + Input: new(dto.IDReq), + Output: new(dto.AgentRechargeResponse), + Auth: true, + }) + + Register(group, doc, groupPath, "POST", "/:id/offline-pay", handler.OfflinePay, RouteSpec{ + Summary: "确认线下充值", + Tags: []string{"代理预充值"}, + Input: new(dto.AgentOfflinePayRequest), + Output: new(dto.AgentRechargeResponse), + Auth: true, + }) +} diff --git a/internal/service/agent_recharge/service.go b/internal/service/agent_recharge/service.go new file mode 100644 index 0000000..7f4f962 --- /dev/null +++ b/internal/service/agent_recharge/service.go @@ -0,0 +1,506 @@ +// 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 +}