package wallet import ( "context" "sort" "strings" "hyapp/pkg/appcode" "hyapp/pkg/xerr" "hyapp/services/wallet-service/internal/domain/ledger" ) // GetRedPacketConfig 返回红包配置;未配置时 repository 会返回关闭状态。 func (s *Service) GetRedPacketConfig(ctx context.Context, appCode string) (ledger.RedPacketConfig, error) { if s.repository == nil { return ledger.RedPacketConfig{}, xerr.New(xerr.Unavailable, "wallet repository is not configured") } appCode = appcode.Normalize(appCode) ctx = appcode.WithContext(ctx, appCode) return s.repository.GetRedPacketConfig(ctx, appCode) } // UpdateRedPacketConfig 保存后台红包配置;时间和档位必须在 wallet-service 内强校验。 func (s *Service) UpdateRedPacketConfig(ctx context.Context, config ledger.RedPacketConfig) (ledger.RedPacketConfig, error) { if s.repository == nil { return ledger.RedPacketConfig{}, xerr.New(xerr.Unavailable, "wallet repository is not configured") } config.AppCode = appcode.Normalize(config.AppCode) config.CountTiers = normalizeCountTiers(config.CountTiers) config.AmountTiers = normalizeAmountTiers(config.AmountTiers) config.RuleURL = strings.TrimSpace(config.RuleURL) config.ExpireSeconds = ledger.RedPacketExpireSeconds if config.UpdatedByAdminID <= 0 { return ledger.RedPacketConfig{}, xerr.New(xerr.InvalidArgument, "operator_user_id is required") } if len(config.CountTiers) == 0 || len(config.AmountTiers) == 0 { return ledger.RedPacketConfig{}, xerr.New(xerr.InvalidArgument, "red packet tiers are required") } if config.DelayedOpenSeconds < 0 || config.DailySendLimit < 0 { return ledger.RedPacketConfig{}, xerr.New(xerr.InvalidArgument, "red packet config is invalid") } if config.DelayedOpenSeconds >= ledger.RedPacketExpireSeconds { return ledger.RedPacketConfig{}, xerr.New(xerr.InvalidArgument, "red packet expire_seconds must be greater than delayed_open_seconds") } if len(config.CountTiers) > 0 && len(config.AmountTiers) > 0 && config.AmountTiers[0] < int64(config.CountTiers[len(config.CountTiers)-1]) { return ledger.RedPacketConfig{}, xerr.New(xerr.InvalidArgument, "red packet amount tiers must cover count tiers") } ctx = appcode.WithContext(ctx, config.AppCode) return s.repository.UpdateRedPacketConfig(ctx, config) } // CreateRedPacket 发普通或延迟红包;扣款、随机拆分和 outbox 都由 repository 事务完成。 func (s *Service) CreateRedPacket(ctx context.Context, command ledger.RedPacketCreateCommand) (ledger.RedPacketCreateReceipt, error) { if s.repository == nil { return ledger.RedPacketCreateReceipt{}, xerr.New(xerr.Unavailable, "wallet repository is not configured") } command.AppCode = appcode.Normalize(command.AppCode) command.CommandID = strings.TrimSpace(command.CommandID) command.RoomID = strings.TrimSpace(command.RoomID) command.PacketType = ledger.NormalizeRedPacketType(command.PacketType) if command.CommandID == "" || command.SenderUserID <= 0 || command.RoomID == "" || command.RegionID <= 0 { return ledger.RedPacketCreateReceipt{}, xerr.New(xerr.InvalidArgument, "red packet command is incomplete") } if command.PacketType == "" { return ledger.RedPacketCreateReceipt{}, xerr.New(xerr.InvalidArgument, "red packet type is invalid") } if command.TotalAmount <= 0 || command.PacketCount <= 0 { return ledger.RedPacketCreateReceipt{}, xerr.New(xerr.InvalidArgument, "red packet amount and count are required") } if command.TotalAmount < int64(command.PacketCount) { return ledger.RedPacketCreateReceipt{}, xerr.New(xerr.RedPacketAmountTooSmall, "red packet amount is too small") } ctx = appcode.WithContext(ctx, command.AppCode) return s.repository.CreateRedPacket(ctx, command) } // ClaimRedPacket 抢一个红包份额;同一个用户同一红包只允许成功一次。 func (s *Service) ClaimRedPacket(ctx context.Context, command ledger.RedPacketClaimCommand) (ledger.RedPacketClaimReceipt, error) { if s.repository == nil { return ledger.RedPacketClaimReceipt{}, xerr.New(xerr.Unavailable, "wallet repository is not configured") } command.AppCode = appcode.Normalize(command.AppCode) command.CommandID = strings.TrimSpace(command.CommandID) command.PacketID = strings.TrimSpace(command.PacketID) if command.CommandID == "" || command.PacketID == "" || command.UserID <= 0 { return ledger.RedPacketClaimReceipt{}, xerr.New(xerr.InvalidArgument, "red packet claim command is incomplete") } ctx = appcode.WithContext(ctx, command.AppCode) return s.repository.ClaimRedPacket(ctx, command) } func (s *Service) ListRedPackets(ctx context.Context, query ledger.RedPacketQuery) ([]ledger.RedPacket, int64, error) { if s.repository == nil { return nil, 0, xerr.New(xerr.Unavailable, "wallet repository is not configured") } query.AppCode = appcode.Normalize(query.AppCode) query.RoomID = strings.TrimSpace(query.RoomID) query.PacketType = ledger.NormalizeRedPacketType(query.PacketType) query.Status = ledger.NormalizeRedPacketStatus(query.Status) if query.Page <= 0 { query.Page = 1 } if query.PageSize <= 0 { query.PageSize = 20 } if query.PageSize > 100 { query.PageSize = 100 } ctx = appcode.WithContext(ctx, query.AppCode) return s.repository.ListRedPackets(ctx, query) } func (s *Service) GetRedPacket(ctx context.Context, appCode string, packetID string, viewerUserID int64, includeClaims bool) (ledger.RedPacket, error) { if s.repository == nil { return ledger.RedPacket{}, xerr.New(xerr.Unavailable, "wallet repository is not configured") } packetID = strings.TrimSpace(packetID) if packetID == "" { return ledger.RedPacket{}, xerr.New(xerr.InvalidArgument, "packet_id is required") } appCode = appcode.Normalize(appCode) ctx = appcode.WithContext(ctx, appCode) return s.repository.GetRedPacket(ctx, appCode, packetID, viewerUserID, includeClaims) } func (s *Service) ExpireRedPackets(ctx context.Context, appCode string, batchSize int32) (ledger.RedPacketExpireResult, error) { if s.repository == nil { return ledger.RedPacketExpireResult{}, xerr.New(xerr.Unavailable, "wallet repository is not configured") } appCode = appcode.Normalize(appCode) ctx = appcode.WithContext(ctx, appCode) return s.repository.ExpireRedPackets(ctx, appCode, batchSize) } func (s *Service) RetryRedPacketRefund(ctx context.Context, appCode string, packetID string, operatorUserID int64) (ledger.RedPacketRefundReceipt, error) { if s.repository == nil { return ledger.RedPacketRefundReceipt{}, xerr.New(xerr.Unavailable, "wallet repository is not configured") } packetID = strings.TrimSpace(packetID) if packetID == "" || operatorUserID <= 0 { return ledger.RedPacketRefundReceipt{}, xerr.New(xerr.InvalidArgument, "red packet refund retry command is incomplete") } appCode = appcode.Normalize(appCode) ctx = appcode.WithContext(ctx, appCode) return s.repository.RetryRedPacketRefund(ctx, appCode, packetID, operatorUserID) } func normalizeCountTiers(values []int32) []int32 { seen := make(map[int32]bool, len(values)) out := make([]int32, 0, len(values)) for _, value := range values { if value <= 0 || seen[value] { continue } seen[value] = true out = append(out, value) } sort.Slice(out, func(i, j int) bool { return out[i] < out[j] }) return out } func normalizeAmountTiers(values []int64) []int64 { seen := make(map[int64]bool, len(values)) out := make([]int64, 0, len(values)) for _, value := range values { if value <= 0 || seen[value] { continue } seen[value] = true out = append(out, value) } sort.Slice(out, func(i, j int) bool { return out[i] < out[j] }) return out }