package wallet import ( "context" "strings" "time" "hyapp/pkg/appcode" "hyapp/pkg/xerr" "hyapp/services/wallet-service/internal/domain/ledger" ) const vipDailyCoinRebateTaskDayLayout = "2006-01-02" // ProcessVipDailyCoinRebateBatch 只接受 UTC 日期,不接受时区偏移。空 task_day 使用 wallet owner // 当前 UTC 日期,使调度方无需也不能自行决定业务切日。 func (s *Service) ProcessVipDailyCoinRebateBatch(ctx context.Context, command ledger.ProcessVipDailyCoinRebateBatchCommand) (ledger.ProcessVipDailyCoinRebateBatchResult, error) { if s.repository == nil { return ledger.ProcessVipDailyCoinRebateBatchResult{}, xerr.New(xerr.Unavailable, "wallet repository is not configured") } command.AppCode = appcode.Normalize(command.AppCode) now := s.now().UTC() command.NowMS = now.UnixMilli() command.TaskDay = strings.TrimSpace(command.TaskDay) if command.TaskDay == "" { command.TaskDay = now.Format(vipDailyCoinRebateTaskDayLayout) } dayStart, err := time.ParseInLocation(vipDailyCoinRebateTaskDayLayout, command.TaskDay, time.UTC) if err != nil || dayStart.Format(vipDailyCoinRebateTaskDayLayout) != command.TaskDay { return ledger.ProcessVipDailyCoinRebateBatchResult{}, xerr.New(xerr.InvalidArgument, "task_day must be an UTC date in YYYY-MM-DD") } if dayStart.After(now) { return ledger.ProcessVipDailyCoinRebateBatchResult{}, xerr.New(xerr.InvalidArgument, "future vip rebate task_day is not allowed") } if command.BatchSize <= 0 { command.BatchSize = 100 } if command.BatchSize > 500 { command.BatchSize = 500 } ctx = appcode.WithContext(ctx, command.AppCode) return s.repository.ProcessVipDailyCoinRebateBatch(ctx, command) } // GetMyCurrentVipDailyCoinRebate 查询当前 UTC 日返现;没有记录是正常状态,以 found=false 表达。 func (s *Service) GetMyCurrentVipDailyCoinRebate(ctx context.Context, userID int64) (ledger.VipDailyCoinRebate, bool, error) { if userID <= 0 { return ledger.VipDailyCoinRebate{}, false, xerr.New(xerr.InvalidArgument, "user_id is required") } if s.repository == nil { return ledger.VipDailyCoinRebate{}, false, xerr.New(xerr.Unavailable, "wallet repository is not configured") } nowMS := s.now().UTC().UnixMilli() ctx = appcode.WithContext(ctx, appcode.FromContext(ctx)) return s.repository.GetMyCurrentVipDailyCoinRebate(ctx, userID, nowMS) } // ListMyVipDailyCoinRebateStatuses 返回用户历史记录;expired 由 wallet owner 使用当前 UTC 时钟投影。 func (s *Service) ListMyVipDailyCoinRebateStatuses(ctx context.Context, query ledger.ListVipDailyCoinRebateStatusesQuery) ([]ledger.VipDailyCoinRebate, int64, error) { if query.UserID <= 0 { return nil, 0, xerr.New(xerr.InvalidArgument, "user_id is required") } if s.repository == nil { return nil, 0, xerr.New(xerr.Unavailable, "wallet repository is not configured") } query.AppCode = appcode.Normalize(query.AppCode) if query.Page <= 0 { query.Page = 1 } if query.PageSize <= 0 { query.PageSize = 20 } if query.PageSize > 100 { query.PageSize = 100 } if len(query.RebateIDs) > 100 { return nil, 0, xerr.New(xerr.InvalidArgument, "rebate_ids cannot contain more than 100 items") } seenRebateIDs := make(map[string]struct{}, len(query.RebateIDs)) normalizedRebateIDs := make([]string, 0, len(query.RebateIDs)) for _, rebateID := range query.RebateIDs { rebateID = strings.TrimSpace(rebateID) if rebateID == "" || len(rebateID) > 96 { return nil, 0, xerr.New(xerr.InvalidArgument, "rebate_ids contains an invalid id") } if _, exists := seenRebateIDs[rebateID]; exists { continue } seenRebateIDs[rebateID] = struct{}{} normalizedRebateIDs = append(normalizedRebateIDs, rebateID) } query.RebateIDs = normalizedRebateIDs query.NowMS = s.now().UTC().UnixMilli() ctx = appcode.WithContext(ctx, query.AppCode) return s.repository.ListMyVipDailyCoinRebateStatuses(ctx, query) } // ClaimVipDailyCoinRebate 只校验命令形状;可领取窗口、金额、付费 VIP 快照和余额变更均在 // repository 的同一行锁事务内裁决,客户端无法提交金额或延长过期时间。 func (s *Service) ClaimVipDailyCoinRebate(ctx context.Context, command ledger.ClaimVipDailyCoinRebateCommand) (ledger.ClaimVipDailyCoinRebateReceipt, error) { command.AppCode = appcode.Normalize(command.AppCode) command.CommandID = strings.TrimSpace(command.CommandID) command.RebateID = strings.TrimSpace(command.RebateID) if command.CommandID == "" || command.UserID <= 0 || command.RebateID == "" { return ledger.ClaimVipDailyCoinRebateReceipt{}, xerr.New(xerr.InvalidArgument, "vip daily coin rebate claim command is incomplete") } if len(command.CommandID) > 128 || len(command.RebateID) > 96 { return ledger.ClaimVipDailyCoinRebateReceipt{}, xerr.New(xerr.InvalidArgument, "vip daily coin rebate claim command is invalid") } if s.repository == nil { return ledger.ClaimVipDailyCoinRebateReceipt{}, xerr.New(xerr.Unavailable, "wallet repository is not configured") } command.NowMS = s.now().UTC().UnixMilli() ctx = appcode.WithContext(ctx, command.AppCode) return s.repository.ClaimVipDailyCoinRebate(ctx, command) }