package wallet import ( "context" "hyapp/pkg/appcode" "hyapp/pkg/xerr" "hyapp/services/wallet-service/internal/domain/ledger" "strings" ) // CreditTaskReward 发放任务奖励资产;任务完成判断属于 activity-service,这里只负责账本幂等入账。 func (s *Service) CreditTaskReward(ctx context.Context, command ledger.TaskRewardCommand) (ledger.TaskRewardReceipt, error) { if command.CommandID == "" || command.TargetUserID <= 0 || command.Amount <= 0 || command.TaskID == "" || command.CycleKey == "" { return ledger.TaskRewardReceipt{}, xerr.New(xerr.InvalidArgument, "task reward command is incomplete") } command.AssetType = strings.ToUpper(strings.TrimSpace(command.AssetType)) if command.AssetType == "" { command.AssetType = ledger.AssetCoin } if command.AssetType != ledger.AssetCoin && command.AssetType != ledger.AssetPoint { return ledger.TaskRewardReceipt{}, xerr.New(xerr.InvalidArgument, "task reward asset_type is invalid") } command.TaskType = strings.ToLower(strings.TrimSpace(command.TaskType)) if command.TaskType != "daily" && command.TaskType != "exclusive" { return ledger.TaskRewardReceipt{}, xerr.New(xerr.InvalidArgument, "task_type is invalid") } command.Reason = strings.TrimSpace(command.Reason) if command.Reason == "" { return ledger.TaskRewardReceipt{}, xerr.New(xerr.InvalidArgument, "reason is required") } if s.repository == nil { return ledger.TaskRewardReceipt{}, xerr.New(xerr.Unavailable, "wallet repository is not configured") } command.AppCode = appcode.Normalize(command.AppCode) if command.AssetType == ledger.AssetPoint && command.AppCode != "huwaa" { // POINT 是 Huwaa 策略资产;钱包服务自身也做 App 隔离,避免后台或 activity 配错后污染其它 App 账本。 return ledger.TaskRewardReceipt{}, xerr.New(xerr.InvalidArgument, "POINT task reward is only enabled for huwaa") } ctx = appcode.WithContext(ctx, command.AppCode) return s.repository.CreditTaskReward(ctx, command) } // CreditLuckyGiftReward 发放幸运礼物中奖金币;抽奖资格和金额已由 activity-service 决策,钱包只做幂等入账。 func (s *Service) CreditLuckyGiftReward(ctx context.Context, command ledger.LuckyGiftRewardCommand) (ledger.LuckyGiftRewardReceipt, error) { if command.CommandID == "" || command.TargetUserID <= 0 || command.Amount <= 0 || command.DrawID == "" || command.RoomID == "" || command.GiftID == "" || command.PoolID == "" { return ledger.LuckyGiftRewardReceipt{}, xerr.New(xerr.InvalidArgument, "lucky gift reward command is incomplete") } command.Reason = strings.TrimSpace(command.Reason) if command.Reason == "" { return ledger.LuckyGiftRewardReceipt{}, xerr.New(xerr.InvalidArgument, "reason is required") } if s.repository == nil { return ledger.LuckyGiftRewardReceipt{}, xerr.New(xerr.Unavailable, "wallet repository is not configured") } command.AppCode = appcode.Normalize(command.AppCode) command.DrawID = strings.TrimSpace(command.DrawID) command.RoomID = strings.TrimSpace(command.RoomID) command.GiftID = strings.TrimSpace(command.GiftID) command.PoolID = strings.TrimSpace(command.PoolID) ctx = appcode.WithContext(ctx, command.AppCode) return s.repository.CreditLuckyGiftReward(ctx, command) } // CreditWheelReward 发放转盘金币奖品;抽奖、RTP 和奖品命中归 activity-service,钱包只负责独立 reason 的幂等入账。 func (s *Service) CreditWheelReward(ctx context.Context, command ledger.WheelRewardCommand) (ledger.WheelRewardReceipt, error) { if command.CommandID == "" || command.TargetUserID <= 0 || command.Amount <= 0 || command.DrawID == "" || command.WheelID == "" || command.SelectedTierID == "" { return ledger.WheelRewardReceipt{}, xerr.New(xerr.InvalidArgument, "wheel reward command is incomplete") } command.Reason = strings.TrimSpace(command.Reason) if command.Reason == "" { return ledger.WheelRewardReceipt{}, xerr.New(xerr.InvalidArgument, "reason is required") } if s.repository == nil { return ledger.WheelRewardReceipt{}, xerr.New(xerr.Unavailable, "wallet repository is not configured") } command.AppCode = appcode.Normalize(command.AppCode) command.DrawID = strings.TrimSpace(command.DrawID) command.WheelID = strings.TrimSpace(command.WheelID) command.SelectedTierID = strings.TrimSpace(command.SelectedTierID) ctx = appcode.WithContext(ctx, command.AppCode) return s.repository.CreditWheelReward(ctx, command) } // CreditRoomTurnoverReward 发放每周房间流水奖励金币;结算归 activity-service,钱包只负责幂等入账。 func (s *Service) CreditRoomTurnoverReward(ctx context.Context, command ledger.RoomTurnoverRewardCommand) (ledger.RoomTurnoverRewardReceipt, error) { // 钱包只接受 activity-service 已经生成好的结算命令;缺 settlement、room 或正向金额时直接拒绝,避免钱包自己推断活动规则。 if command.CommandID == "" || command.TargetUserID <= 0 || command.Amount <= 0 || command.SettlementID == "" || command.RoomID == "" { return ledger.RoomTurnoverRewardReceipt{}, xerr.New(xerr.InvalidArgument, "room turnover reward command is incomplete") } // 周期、流水和档位是账务元数据的一部分,必须随交易落库,后续对账才能从钱包交易反查是哪一周、哪个房间、哪个档位。 if command.PeriodStartMS <= 0 || command.PeriodEndMS <= command.PeriodStartMS || command.CoinSpent <= 0 || command.TierID <= 0 { return ledger.RoomTurnoverRewardReceipt{}, xerr.New(xerr.InvalidArgument, "room turnover reward period or tier is invalid") } command.Reason = strings.TrimSpace(command.Reason) if command.Reason == "" { return ledger.RoomTurnoverRewardReceipt{}, xerr.New(xerr.InvalidArgument, "reason is required") } if s.repository == nil { return ledger.RoomTurnoverRewardReceipt{}, xerr.New(xerr.Unavailable, "wallet repository is not configured") } command.AppCode = appcode.Normalize(command.AppCode) command.SettlementID = strings.TrimSpace(command.SettlementID) command.RoomID = strings.TrimSpace(command.RoomID) command.TierCode = strings.TrimSpace(command.TierCode) // AppCode 归一化后写入 context,让 repository 的交易、账户和 outbox 都落到同一个 App 分区。 ctx = appcode.WithContext(ctx, command.AppCode) return s.repository.CreditRoomTurnoverReward(ctx, command) } // CreditInviteActivityReward 发放邀请活动金币奖励;达标和重复领取判断属于 activity-service,钱包只负责幂等入账。 func (s *Service) CreditInviteActivityReward(ctx context.Context, command ledger.InviteActivityRewardCommand) (ledger.InviteActivityRewardReceipt, error) { // 钱包入口再次校验最小账务语义,避免上游 bug 生成空 claim 或 0 金额交易。 if command.CommandID == "" || command.TargetUserID <= 0 || command.Amount <= 0 || command.ClaimID == "" || command.TierID <= 0 || command.CycleKey == "" { return ledger.InviteActivityRewardReceipt{}, xerr.New(xerr.InvalidArgument, "invite activity reward command is incomplete") } command.RewardType = strings.ToLower(strings.TrimSpace(command.RewardType)) if command.RewardType != "recharge" && command.RewardType != "valid_invite" && command.RewardType != "invite_inviter" && command.RewardType != "invite_invitee" { return ledger.InviteActivityRewardReceipt{}, xerr.New(xerr.InvalidArgument, "invite activity reward_type is invalid") } command.Reason = strings.TrimSpace(command.Reason) if command.Reason == "" { return ledger.InviteActivityRewardReceipt{}, xerr.New(xerr.InvalidArgument, "reason is required") } if s.repository == nil { return ledger.InviteActivityRewardReceipt{}, xerr.New(xerr.Unavailable, "wallet repository is not configured") } command.AppCode = appcode.Normalize(command.AppCode) command.ClaimID = strings.TrimSpace(command.ClaimID) command.TierCode = strings.TrimSpace(command.TierCode) command.CycleKey = strings.TrimSpace(command.CycleKey) ctx = appcode.WithContext(ctx, command.AppCode) return s.repository.CreditInviteActivityReward(ctx, command) } // CreditAgencyOpeningReward 发放代理开业流水档位金币奖励;命中的档位序号和金额由 activity-service 固化,钱包只做幂等入账。 func (s *Service) CreditAgencyOpeningReward(ctx context.Context, command ledger.AgencyOpeningRewardCommand) (ledger.AgencyOpeningRewardReceipt, error) { if command.CommandID == "" || command.TargetUserID <= 0 || command.Amount <= 0 || command.ApplicationID == "" || command.CycleID == "" || command.AgencyID <= 0 || command.RankNo <= 0 { return ledger.AgencyOpeningRewardReceipt{}, xerr.New(xerr.InvalidArgument, "agency opening reward command is incomplete") } if command.ScoreCoins <= 0 { return ledger.AgencyOpeningRewardReceipt{}, xerr.New(xerr.InvalidArgument, "agency opening score is required") } command.Reason = strings.TrimSpace(command.Reason) if command.Reason == "" { return ledger.AgencyOpeningRewardReceipt{}, xerr.New(xerr.InvalidArgument, "reason is required") } if s.repository == nil { return ledger.AgencyOpeningRewardReceipt{}, xerr.New(xerr.Unavailable, "wallet repository is not configured") } command.AppCode = appcode.Normalize(command.AppCode) command.ApplicationID = strings.TrimSpace(command.ApplicationID) command.CycleID = strings.TrimSpace(command.CycleID) ctx = appcode.WithContext(ctx, command.AppCode) return s.repository.CreditAgencyOpeningReward(ctx, command) } // DebitCPBreakupFee 扣除解除关系费用;关系是否可解除仍由 user-service 的 pending/confirm 流程决定。 func (s *Service) DebitCPBreakupFee(ctx context.Context, command ledger.CPBreakupFeeCommand) (ledger.CPBreakupFeeReceipt, error) { if command.CommandID == "" || command.UserID <= 0 || strings.TrimSpace(command.RelationshipID) == "" || strings.TrimSpace(command.RelationType) == "" { return ledger.CPBreakupFeeReceipt{}, xerr.New(xerr.InvalidArgument, "cp breakup fee command is incomplete") } if len(command.CommandID) > 128 { return ledger.CPBreakupFeeReceipt{}, xerr.New(xerr.InvalidArgument, "command_id is too long") } if command.Amount <= 0 { return ledger.CPBreakupFeeReceipt{}, xerr.New(xerr.InvalidArgument, "cp breakup fee amount must be positive") } if s.repository == nil { return ledger.CPBreakupFeeReceipt{}, xerr.New(xerr.Unavailable, "wallet repository is not configured") } command.AppCode = appcode.Normalize(command.AppCode) command.RelationshipID = strings.TrimSpace(command.RelationshipID) command.RelationType = strings.ToLower(strings.TrimSpace(command.RelationType)) ctx = appcode.WithContext(ctx, command.AppCode) return s.repository.DebitCPBreakupFee(ctx, command) } // DebitWheelDraw 扣除转盘抽奖消耗金币;抽奖命中必须在 activity-service 完成,wallet 只提供可幂等复用的扣费事实。 func (s *Service) DebitWheelDraw(ctx context.Context, command ledger.WheelDrawDebitCommand) (ledger.WheelDrawDebitReceipt, error) { if command.CommandID == "" || command.UserID <= 0 || strings.TrimSpace(command.WheelID) == "" { return ledger.WheelDrawDebitReceipt{}, xerr.New(xerr.InvalidArgument, "wheel draw debit command is incomplete") } if len(command.CommandID) > 128 { return ledger.WheelDrawDebitReceipt{}, xerr.New(xerr.InvalidArgument, "command_id is too long") } if command.DrawCount <= 0 { return ledger.WheelDrawDebitReceipt{}, xerr.New(xerr.InvalidArgument, "draw_count must be positive") } if command.Amount <= 0 { return ledger.WheelDrawDebitReceipt{}, xerr.New(xerr.InvalidArgument, "wheel draw debit amount must be positive") } if s.repository == nil { return ledger.WheelDrawDebitReceipt{}, xerr.New(xerr.Unavailable, "wallet repository is not configured") } command.AppCode = appcode.Normalize(command.AppCode) command.WheelID = strings.TrimSpace(command.WheelID) ctx = appcode.WithContext(ctx, command.AppCode) return s.repository.DebitWheelDraw(ctx, command) }