package grpc import ( "context" "time" walletv1 "hyapp.local/api/proto/wallet/v1" "hyapp/pkg/appcode" "hyapp/pkg/xerr" "hyapp/services/wallet-service/internal/domain/ledger" walletservice "hyapp/services/wallet-service/internal/service/wallet" ) // Server 把 wallet-service 用例层适配为 gRPC 接口。 type Server struct { walletv1.UnimplementedWalletServiceServer svc *walletservice.Service } // NewServer 创建 wallet gRPC server。 func NewServer(svc *walletservice.Service) *Server { return &Server{svc: svc} } // DebitGift 只负责 protobuf 转换和错误映射,扣费规则在 service/repository。 func (s *Server) DebitGift(ctx context.Context, req *walletv1.DebitGiftRequest) (*walletv1.DebitGiftResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) receipt, err := s.svc.DebitGift(ctx, ledger.DebitGiftCommand{ AppCode: req.GetAppCode(), CommandID: req.GetCommandId(), RoomID: req.GetRoomId(), SenderUserID: req.GetSenderUserId(), TargetUserID: req.GetTargetUserId(), GiftID: req.GetGiftId(), GiftCount: req.GetGiftCount(), PriceVersion: req.GetPriceVersion(), RegionID: req.GetRegionId(), SenderRegionID: req.GetSenderRegionId(), TargetIsHost: req.GetTargetIsHost(), // 工资政策区域来自 user-service host profile,不能用房间 visible_region_id 兜底。 TargetHostRegionID: req.GetTargetHostRegionId(), TargetAgencyOwnerUserID: req.GetTargetAgencyOwnerUserId(), }) if err != nil { return nil, xerr.ToGRPCError(err) } return debitGiftResponseFromReceipt(receipt), nil } // BatchDebitGift 只负责批量目标 protobuf 转换;同事务扣费和幂等由 wallet service/repository 保证。 func (s *Server) BatchDebitGift(ctx context.Context, req *walletv1.BatchDebitGiftRequest) (*walletv1.BatchDebitGiftResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) targets := make([]ledger.DebitGiftTargetCommand, 0, len(req.GetTargets())) for _, target := range req.GetTargets() { targets = append(targets, ledger.DebitGiftTargetCommand{ CommandID: target.GetCommandId(), TargetUserID: target.GetTargetUserId(), TargetIsHost: target.GetTargetIsHost(), TargetHostRegionID: target.GetTargetHostRegionId(), TargetAgencyOwnerUserID: target.GetTargetAgencyOwnerUserId(), }) } receipt, err := s.svc.BatchDebitGift(ctx, ledger.BatchDebitGiftCommand{ AppCode: req.GetAppCode(), CommandID: req.GetCommandId(), RoomID: req.GetRoomId(), SenderUserID: req.GetSenderUserId(), GiftID: req.GetGiftId(), GiftCount: req.GetGiftCount(), PriceVersion: req.GetPriceVersion(), RegionID: req.GetRegionId(), SenderRegionID: req.GetSenderRegionId(), Targets: targets, }) if err != nil { return nil, xerr.ToGRPCError(err) } response := &walletv1.BatchDebitGiftResponse{ Aggregate: debitGiftResponseFromReceipt(receipt.Aggregate), Receipts: make([]*walletv1.BatchDebitGiftReceipt, 0, len(receipt.Targets)), } for _, target := range receipt.Targets { response.Receipts = append(response.Receipts, &walletv1.BatchDebitGiftReceipt{ TargetUserId: target.TargetUserID, CommandId: target.CommandID, Billing: debitGiftResponseFromReceipt(target.Receipt), }) } return response, nil } // DebitRobotGift 扣机器人专用金币;该入口只供内部机器人房间编排链路调用。 func (s *Server) DebitRobotGift(ctx context.Context, req *walletv1.DebitRobotGiftRequest) (*walletv1.DebitGiftResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) receipt, err := s.svc.DebitRobotGift(ctx, ledger.DebitGiftCommand{ AppCode: req.GetAppCode(), CommandID: req.GetCommandId(), RoomID: req.GetRoomId(), SenderUserID: req.GetSenderUserId(), TargetUserID: req.GetTargetUserId(), GiftID: req.GetGiftId(), GiftCount: req.GetGiftCount(), PriceVersion: req.GetPriceVersion(), RegionID: req.GetRegionId(), SenderRegionID: req.GetSenderRegionId(), }) if err != nil { return nil, xerr.ToGRPCError(err) } return debitGiftResponseFromReceipt(receipt), nil } func debitGiftResponseFromReceipt(receipt ledger.Receipt) *walletv1.DebitGiftResponse { return &walletv1.DebitGiftResponse{ BillingReceiptId: receipt.BillingReceiptID, BalanceAfter: receipt.BalanceAfter, TransactionId: receipt.TransactionID, CoinSpent: receipt.CoinSpent, ChargeAssetType: receipt.ChargeAssetType, ChargeAmount: receipt.ChargeAmount, GiftPointAdded: receipt.GiftPointAdded, HeatValue: receipt.HeatValue, GiftTypeCode: receipt.GiftTypeCode, CpRelationType: receipt.CPRelationType, GiftName: receipt.GiftName, GiftIconUrl: receipt.GiftIconURL, GiftAnimationUrl: receipt.GiftAnimationURL, GiftEffectTypes: receipt.GiftEffectTypes, PriceVersion: receipt.PriceVersion, HostPeriodDiamondAdded: receipt.HostPeriodDiamondAdded, HostPeriodCycleKey: receipt.HostPeriodCycleKey, } } // GetBalances 返回用户多资产余额投影。 func (s *Server) GetBalances(ctx context.Context, req *walletv1.GetBalancesRequest) (*walletv1.GetBalancesResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) balances, err := s.svc.GetBalances(ctx, req.GetUserId(), req.GetAssetTypes()) if err != nil { return nil, xerr.ToGRPCError(err) } response := &walletv1.GetBalancesResponse{Balances: make([]*walletv1.AssetBalance, 0, len(balances))} for _, balance := range balances { response.Balances = append(response.Balances, &walletv1.AssetBalance{ AppCode: balance.AppCode, AssetType: balance.AssetType, AvailableAmount: balance.AvailableAmount, FrozenAmount: balance.FrozenAmount, Version: balance.Version, }) } return response, nil } // GetActiveHostSalaryPolicy 读取当前生效的主播工资政策,供 gateway 按当前主播区域展示平台规则。 func (s *Server) GetActiveHostSalaryPolicy(ctx context.Context, req *walletv1.GetActiveHostSalaryPolicyRequest) (*walletv1.GetActiveHostSalaryPolicyResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) policy, found, err := s.svc.GetActiveHostSalaryPolicy(ctx, req.GetAppCode(), req.GetRegionId(), req.GetSettlementMode(), req.GetSettlementTriggerMode(), req.GetNowMs()) if err != nil { return nil, xerr.ToGRPCError(err) } response := &walletv1.GetActiveHostSalaryPolicyResponse{Found: found} if found { response.Policy = hostSalaryPolicyToProto(policy) } return response, nil } // GetHostSalaryProgress 读取主播当前工资周期钻石累计,gateway 用它计算工资政策等级进度。 func (s *Server) GetHostSalaryProgress(ctx context.Context, req *walletv1.GetHostSalaryProgressRequest) (*walletv1.GetHostSalaryProgressResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) progress, err := s.svc.GetHostSalaryProgress(ctx, req.GetAppCode(), req.GetHostUserId(), req.GetCycleKey(), req.GetNowMs()) if err != nil { return nil, xerr.ToGRPCError(err) } return &walletv1.GetHostSalaryProgressResponse{Progress: hostSalaryProgressToProto(progress)}, nil } func hostSalaryProgressToProto(progress ledger.HostSalaryProgress) *walletv1.HostSalaryProgress { return &walletv1.HostSalaryProgress{ HostUserId: progress.HostUserID, CycleKey: progress.CycleKey, RegionId: progress.RegionID, AgencyOwnerUserId: progress.AgencyOwnerUserID, TotalDiamonds: progress.TotalDiamonds, GiftDiamondTotal: progress.GiftDiamondTotal, UpdatedAtMs: progress.UpdatedAtMS, } } func hostSalaryPolicyToProto(policy ledger.HostSalaryPolicy) *walletv1.HostSalaryPolicy { levels := make([]*walletv1.HostSalaryPolicyLevel, 0, len(policy.Levels)) for _, level := range policy.Levels { levels = append(levels, hostSalaryPolicyLevelToProto(level)) } return &walletv1.HostSalaryPolicy{ PolicyId: policy.PolicyID, Name: policy.Name, RegionId: policy.RegionID, Status: policy.Status, SettlementMode: policy.SettlementMode, SettlementTriggerMode: policy.SettlementTriggerMode, GiftCoinToDiamondRatio: policy.GiftCoinToDiamondRatio, ResidualDiamondToUsdRate: policy.ResidualDiamondToUSDRate, EffectiveFromMs: policy.EffectiveFromMs, EffectiveToMs: policy.EffectiveToMs, Levels: levels, } } func hostSalaryPolicyLevelToProto(level ledger.HostSalaryPolicyLevel) *walletv1.HostSalaryPolicyLevel { return &walletv1.HostSalaryPolicyLevel{ LevelNo: int32(level.LevelNo), RequiredDiamonds: level.RequiredDiamonds, HostSalaryUsdMinor: level.HostSalaryUSDMinor, HostCoinReward: level.HostCoinReward, AgencySalaryUsdMinor: level.AgencySalaryUSDMinor, Status: level.Status, SortOrder: int32(level.SortOrder), } } // GetWalletOverview 返回钱包首页摘要,供我的页和钱包二级页复用。 func (s *Server) GetWalletOverview(ctx context.Context, req *walletv1.GetWalletOverviewRequest) (*walletv1.GetWalletOverviewResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) overview, err := s.svc.GetWalletOverview(ctx, req.GetUserId()) if err != nil { return nil, xerr.ToGRPCError(err) } return &walletv1.GetWalletOverviewResponse{ Balances: balancesToProto(overview.Balances), FeatureFlags: walletFeatureFlagsToProto(overview.FeatureFlags), }, nil } // GetWalletValueSummary 返回我的页钱包卡片最小金币余额。 func (s *Server) GetWalletValueSummary(ctx context.Context, req *walletv1.GetWalletValueSummaryRequest) (*walletv1.GetWalletValueSummaryResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) summary, err := s.svc.GetWalletValueSummary(ctx, req.GetUserId()) if err != nil { return nil, xerr.ToGRPCError(err) } return &walletv1.GetWalletValueSummaryResponse{Summary: &walletv1.WalletValueSummary{ CoinAmount: summary.CoinAmount, UpdatedAtMs: summary.UpdatedAtMS, }}, nil } // GetUserGiftWall 返回当前用户收到礼物的聚合墙;调用方负责限定 user_id 为当前登录用户。 func (s *Server) GetUserGiftWall(ctx context.Context, req *walletv1.GetUserGiftWallRequest) (*walletv1.GetUserGiftWallResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) wall, err := s.svc.GetUserGiftWall(ctx, req.GetUserId()) if err != nil { return nil, xerr.ToGRPCError(err) } resp := &walletv1.GetUserGiftWallResponse{ Items: make([]*walletv1.GiftWallItem, 0, len(wall.Items)), GiftKindCount: wall.GiftKindCount, GiftTotalCount: wall.GiftTotalCount, TotalValue: wall.TotalValue, TotalCoinValue: wall.TotalCoinValue, TotalDiamondValue: wall.TotalDiamondValue, TotalGiftPoint: wall.TotalGiftPoint, TotalHeatValue: wall.TotalHeatValue, } for _, item := range wall.Items { resp.Items = append(resp.Items, giftWallItemToProto(item)) } return resp, nil } // ListRechargeProducts 返回当前用户区域可用的充值渠道和档位。 func (s *Server) ListRechargeProducts(ctx context.Context, req *walletv1.ListRechargeProductsRequest) (*walletv1.ListRechargeProductsResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) products, channels, err := s.svc.ListRechargeProducts(ctx, req.GetUserId(), req.GetRegionId(), req.GetPlatform(), req.GetAudienceType()) if err != nil { return nil, xerr.ToGRPCError(err) } resp := &walletv1.ListRechargeProductsResponse{Channels: channels, Products: make([]*walletv1.RechargeProduct, 0, len(products))} for _, product := range products { resp.Products = append(resp.Products, rechargeProductToProto(product)) } return resp, nil } // ConfirmGooglePayment 校验 Google Play purchase token 并完成钱包入账。 func (s *Server) ConfirmGooglePayment(ctx context.Context, req *walletv1.ConfirmGooglePaymentRequest) (*walletv1.ConfirmGooglePaymentResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) receipt, err := s.svc.ConfirmGooglePayment(ctx, ledger.GooglePaymentCommand{ AppCode: req.GetAppCode(), CommandID: req.GetCommandId(), UserID: req.GetUserId(), RegionID: req.GetRegionId(), ProductID: req.GetProductId(), ProductCode: req.GetProductCode(), PackageName: req.GetPackageName(), PurchaseToken: req.GetPurchaseToken(), OrderID: req.GetOrderId(), PurchaseTimeMS: req.GetPurchaseTimeMs(), }) if err != nil { return nil, xerr.ToGRPCError(err) } return &walletv1.ConfirmGooglePaymentResponse{ PaymentOrderId: receipt.PaymentOrderID, TransactionId: receipt.TransactionID, Status: receipt.Status, ProductId: receipt.ProductID, ProductCode: receipt.ProductCode, CoinAmount: receipt.CoinAmount, Balance: balanceToProto(receipt.Balance), IdempotentReplay: receipt.IdempotentReplay, ConsumeState: receipt.ConsumeState, }, nil } // ListAdminRechargeProducts 返回后台内购配置列表。 func (s *Server) ListAdminRechargeProducts(ctx context.Context, req *walletv1.ListAdminRechargeProductsRequest) (*walletv1.ListAdminRechargeProductsResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) products, total, err := s.svc.ListAdminRechargeProducts(ctx, ledger.ListRechargeProductsQuery{ AppCode: req.GetAppCode(), Status: req.GetStatus(), Platform: req.GetPlatform(), RegionID: req.GetRegionId(), Keyword: req.GetKeyword(), Page: req.GetPage(), PageSize: req.GetPageSize(), AudienceType: req.GetAudienceType(), }) if err != nil { return nil, xerr.ToGRPCError(err) } resp := &walletv1.ListAdminRechargeProductsResponse{Products: make([]*walletv1.RechargeProduct, 0, len(products)), Total: total} for _, product := range products { resp.Products = append(resp.Products, rechargeProductToProto(product)) } return resp, nil } // CreateRechargeProduct 创建后台内购商品配置。 func (s *Server) CreateRechargeProduct(ctx context.Context, req *walletv1.CreateRechargeProductRequest) (*walletv1.RechargeProductResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) product, err := s.svc.CreateRechargeProduct(ctx, ledger.RechargeProductCommand{ AppCode: req.GetAppCode(), AmountMicro: req.GetAmountMicro(), CoinAmount: req.GetCoinAmount(), ProductName: req.GetProductName(), Description: req.GetDescription(), Platform: req.GetPlatform(), RegionIDs: req.GetRegionIds(), Enabled: req.GetEnabled(), OperatorUserID: req.GetOperatorUserId(), AudienceType: req.GetAudienceType(), }) if err != nil { return nil, xerr.ToGRPCError(err) } return &walletv1.RechargeProductResponse{Product: rechargeProductToProto(product)}, nil } // UpdateRechargeProduct 修改后台内购商品配置。 func (s *Server) UpdateRechargeProduct(ctx context.Context, req *walletv1.UpdateRechargeProductRequest) (*walletv1.RechargeProductResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) product, err := s.svc.UpdateRechargeProduct(ctx, ledger.RechargeProductCommand{ AppCode: req.GetAppCode(), ProductID: req.GetProductId(), AmountMicro: req.GetAmountMicro(), CoinAmount: req.GetCoinAmount(), ProductName: req.GetProductName(), Description: req.GetDescription(), Platform: req.GetPlatform(), RegionIDs: req.GetRegionIds(), Enabled: req.GetEnabled(), OperatorUserID: req.GetOperatorUserId(), AudienceType: req.GetAudienceType(), }) if err != nil { return nil, xerr.ToGRPCError(err) } return &walletv1.RechargeProductResponse{Product: rechargeProductToProto(product)}, nil } // DeleteRechargeProduct 删除后台内购商品配置。 func (s *Server) DeleteRechargeProduct(ctx context.Context, req *walletv1.DeleteRechargeProductRequest) (*walletv1.DeleteRechargeProductResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) if err := s.svc.DeleteRechargeProduct(ctx, req.GetAppCode(), req.GetProductId()); err != nil { return nil, xerr.ToGRPCError(err) } return &walletv1.DeleteRechargeProductResponse{Deleted: true}, nil } // ListThirdPartyPaymentChannels 返回三方支付渠道、国家和方式树,后台用它直接渲染二级展开列表。 func (s *Server) ListThirdPartyPaymentChannels(ctx context.Context, req *walletv1.ListThirdPartyPaymentChannelsRequest) (*walletv1.ListThirdPartyPaymentChannelsResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) channels, err := s.svc.ListThirdPartyPaymentChannels(ctx, ledger.ListThirdPartyPaymentChannelsQuery{ AppCode: req.GetAppCode(), ProviderCode: req.GetProviderCode(), Status: req.GetStatus(), IncludeDisabledMethods: req.GetIncludeDisabledMethods(), }) if err != nil { return nil, xerr.ToGRPCError(err) } resp := &walletv1.ListThirdPartyPaymentChannelsResponse{Channels: make([]*walletv1.ThirdPartyPaymentChannel, 0, len(channels))} for _, channel := range channels { resp.Channels = append(resp.Channels, thirdPartyPaymentChannelToProto(channel)) } return resp, nil } // SetThirdPartyPaymentMethodStatus 开关某个国家下的具体支付方式;渠道本身不被连带关闭。 func (s *Server) SetThirdPartyPaymentMethodStatus(ctx context.Context, req *walletv1.SetThirdPartyPaymentMethodStatusRequest) (*walletv1.ThirdPartyPaymentMethodResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) method, err := s.svc.SetThirdPartyPaymentMethodStatus(ctx, ledger.ThirdPartyPaymentMethodStatusCommand{ AppCode: req.GetAppCode(), MethodID: req.GetMethodId(), Enabled: req.GetEnabled(), OperatorUserID: req.GetOperatorUserId(), }) if err != nil { return nil, xerr.ToGRPCError(err) } return &walletv1.ThirdPartyPaymentMethodResponse{Method: thirdPartyPaymentMethodToProto(method)}, nil } // UpdateThirdPartyPaymentRate 保存 USD 到国家本币汇率;H5 只展示已配置正汇率的国家方式。 func (s *Server) UpdateThirdPartyPaymentRate(ctx context.Context, req *walletv1.UpdateThirdPartyPaymentRateRequest) (*walletv1.ThirdPartyPaymentMethodResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) method, err := s.svc.UpdateThirdPartyPaymentRate(ctx, ledger.ThirdPartyPaymentRateCommand{ AppCode: req.GetAppCode(), MethodID: req.GetMethodId(), USDToCurrencyRate: req.GetUsdToCurrencyRate(), OperatorUserID: req.GetOperatorUserId(), }) if err != nil { return nil, xerr.ToGRPCError(err) } return &walletv1.ThirdPartyPaymentMethodResponse{Method: thirdPartyPaymentMethodToProto(method)}, nil } // ListH5RechargeOptions 返回 H5 当前账号的商品和支付方式,普通用户和币商由 audience_type 分流。 func (s *Server) ListH5RechargeOptions(ctx context.Context, req *walletv1.H5RechargeOptionsRequest) (*walletv1.H5RechargeOptionsResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) options, err := s.svc.ListH5RechargeOptions(ctx, ledger.H5RechargeOptionsQuery{ AppCode: req.GetAppCode(), TargetUserID: req.GetTargetUserId(), TargetRegionID: req.GetTargetRegionId(), TargetCountryCode: req.GetTargetCountryCode(), AudienceType: req.GetAudienceType(), }) if err != nil { return nil, xerr.ToGRPCError(err) } resp := &walletv1.H5RechargeOptionsResponse{ Products: make([]*walletv1.RechargeProduct, 0, len(options.Products)), PaymentMethods: make([]*walletv1.ThirdPartyPaymentMethod, 0, len(options.PaymentMethods)), UsdtTrc20Enabled: options.USDTTRC20Enabled, UsdtTrc20Address: options.USDTTRC20Address, } for _, product := range options.Products { resp.Products = append(resp.Products, rechargeProductToProto(product)) } for _, method := range options.PaymentMethods { resp.PaymentMethods = append(resp.PaymentMethods, thirdPartyPaymentMethodToProto(method)) } return resp, nil } // CreateH5RechargeOrder 创建外部充值订单;MiFaPay 只返回 pay_url,USDT 只返回共享收款地址。 func (s *Server) CreateH5RechargeOrder(ctx context.Context, req *walletv1.CreateH5RechargeOrderRequest) (*walletv1.H5RechargeOrderResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) order, err := s.svc.CreateH5RechargeOrder(ctx, ledger.CreateExternalRechargeOrderCommand{ AppCode: req.GetAppCode(), CommandID: req.GetCommandId(), TargetUserID: req.GetTargetUserId(), TargetRegionID: req.GetTargetRegionId(), TargetCountryCode: req.GetTargetCountryCode(), AudienceType: req.GetAudienceType(), ProductID: req.GetProductId(), ProviderCode: req.GetProviderCode(), PaymentMethodID: req.GetPaymentMethodId(), ReturnURL: req.GetReturnUrl(), NotifyURL: req.GetNotifyUrl(), ClientIP: req.GetClientIp(), Language: req.GetLanguage(), PayerName: req.GetPayerName(), PayerAccount: req.GetPayerAccount(), PayerEmail: req.GetPayerEmail(), }) if err != nil { return nil, xerr.ToGRPCError(err) } return &walletv1.H5RechargeOrderResponse{Order: externalRechargeOrderToProto(order)}, nil } // SubmitH5RechargeTx 接收 USDT tx_hash;链上校验和幂等入账仍在 wallet-service 事务里完成。 func (s *Server) SubmitH5RechargeTx(ctx context.Context, req *walletv1.SubmitH5RechargeTxRequest) (*walletv1.H5RechargeOrderResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) order, err := s.svc.SubmitH5RechargeTx(ctx, ledger.SubmitExternalRechargeTxCommand{ AppCode: req.GetAppCode(), OrderID: req.GetOrderId(), TargetUserID: req.GetTargetUserId(), TxHash: req.GetTxHash(), }) if err != nil { return nil, xerr.ToGRPCError(err) } return &walletv1.H5RechargeOrderResponse{Order: externalRechargeOrderToProto(order)}, nil } // GetH5RechargeOrder 给 H5 轮询订单状态;target_user_id 用于避免无 token 场景越权看单。 func (s *Server) GetH5RechargeOrder(ctx context.Context, req *walletv1.GetH5RechargeOrderRequest) (*walletv1.H5RechargeOrderResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) order, err := s.svc.GetH5RechargeOrder(ctx, req.GetAppCode(), req.GetOrderId(), req.GetTargetUserId()) if err != nil { return nil, xerr.ToGRPCError(err) } return &walletv1.H5RechargeOrderResponse{Order: externalRechargeOrderToProto(order)}, nil } // HandleMifapayNotify 验签并处理 MiFaPay 回调;成功或重复成功由 HTTP 层按 response_text 返回大写 SUCCESS。 func (s *Server) HandleMifapayNotify(ctx context.Context, req *walletv1.HandleMifapayNotifyRequest) (*walletv1.HandleMifapayNotifyResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) order, accepted, err := s.svc.HandleMifapayNotify(ctx, req.GetAppCode(), ledger.MifaPayNotification{ MerAccount: req.GetMerAccount(), Data: req.GetData(), Sign: req.GetSign(), }) if err != nil { return nil, xerr.ToGRPCError(err) } responseText := "FAIL" if accepted { responseText = "SUCCESS" } return &walletv1.HandleMifapayNotifyResponse{Accepted: accepted, ResponseText: responseText, OrderId: order.OrderID}, nil } // GetDiamondExchangeConfig 返回钻石兑换配置。 func (s *Server) GetDiamondExchangeConfig(ctx context.Context, req *walletv1.GetDiamondExchangeConfigRequest) (*walletv1.GetDiamondExchangeConfigResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) rules, err := s.svc.GetDiamondExchangeConfig(ctx, req.GetUserId()) if err != nil { return nil, xerr.ToGRPCError(err) } resp := &walletv1.GetDiamondExchangeConfigResponse{Rules: make([]*walletv1.DiamondExchangeRule, 0, len(rules))} for _, rule := range rules { resp.Rules = append(resp.Rules, &walletv1.DiamondExchangeRule{ ExchangeType: rule.ExchangeType, FromAssetType: rule.FromAssetType, ToAssetType: rule.ToAssetType, FromAmount: rule.FromAmount, ToAmount: rule.ToAmount, Enabled: rule.Enabled, }) } return resp, nil } func rechargeProductToProto(product ledger.RechargeProduct) *walletv1.RechargeProduct { return &walletv1.RechargeProduct{ AppCode: product.AppCode, AudienceType: product.AudienceType, ProductId: product.ProductID, ProductCode: product.ProductCode, ProductName: product.ProductName, Description: product.Description, Platform: product.Platform, Channel: product.Channel, CurrencyCode: product.CurrencyCode, AmountMicro: product.AmountMicro, AmountMinor: product.AmountMinor, CoinAmount: product.CoinAmount, PolicyVersion: product.PolicyVersion, Status: product.Status, Enabled: product.Enabled, SortOrder: product.SortOrder, RegionIds: product.RegionIDs, ResourceAssetType: product.ResourceAssetType, CreatedByUserId: product.CreatedByUserID, UpdatedByUserId: product.UpdatedByUserID, CreatedAtMs: product.CreatedAtMS, UpdatedAtMs: product.UpdatedAtMS, } } func thirdPartyPaymentChannelToProto(channel ledger.ThirdPartyPaymentChannel) *walletv1.ThirdPartyPaymentChannel { resp := &walletv1.ThirdPartyPaymentChannel{ AppCode: channel.AppCode, ProviderCode: channel.ProviderCode, ProviderName: channel.ProviderName, Status: channel.Status, SortOrder: channel.SortOrder, Methods: make([]*walletv1.ThirdPartyPaymentMethod, 0, len(channel.Methods)), } for _, method := range channel.Methods { resp.Methods = append(resp.Methods, thirdPartyPaymentMethodToProto(method)) } return resp } func thirdPartyPaymentMethodToProto(method ledger.ThirdPartyPaymentMethod) *walletv1.ThirdPartyPaymentMethod { return &walletv1.ThirdPartyPaymentMethod{ MethodId: method.MethodID, AppCode: method.AppCode, ProviderCode: method.ProviderCode, ProviderName: method.ProviderName, CountryCode: method.CountryCode, CountryName: method.CountryName, CurrencyCode: method.CurrencyCode, PayWay: method.PayWay, PayType: method.PayType, MethodName: method.MethodName, LogoUrl: method.LogoURL, Status: method.Status, UsdToCurrencyRate: method.USDToCurrencyRate, SortOrder: method.SortOrder, CreatedAtMs: method.CreatedAtMS, UpdatedAtMs: method.UpdatedAtMS, } } func externalRechargeOrderToProto(order ledger.ExternalRechargeOrder) *walletv1.ExternalRechargeOrder { return &walletv1.ExternalRechargeOrder{ OrderId: order.OrderID, AppCode: order.AppCode, TargetUserId: order.TargetUserID, TargetRegionId: order.TargetRegionID, TargetCountryCode: order.TargetCountryCode, AudienceType: order.AudienceType, ProductId: order.ProductID, ProductName: order.ProductName, CoinAmount: order.CoinAmount, UsdMinorAmount: order.USDMinorAmount, ProviderCode: order.ProviderCode, PaymentMethodId: order.PaymentMethodID, CountryCode: order.CountryCode, CurrencyCode: order.CurrencyCode, ProviderAmountMinor: order.ProviderAmountMinor, PayWay: order.PayWay, PayType: order.PayType, PayUrl: order.PayURL, ProviderOrderId: order.ProviderOrderID, TxHash: order.TxHash, ReceiveAddress: order.ReceiveAddress, Status: order.Status, FailureReason: order.FailureReason, TransactionId: order.TransactionID, CreatedAtMs: order.CreatedAtMS, UpdatedAtMs: order.UpdatedAtMS, IdempotentReplay: order.IdempotentReplay, } } // ListWalletTransactions 返回当前用户钱包流水。 func (s *Server) ListWalletTransactions(ctx context.Context, req *walletv1.ListWalletTransactionsRequest) (*walletv1.ListWalletTransactionsResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) items, total, err := s.svc.ListWalletTransactions(ctx, ledger.ListWalletTransactionsQuery{ AppCode: req.GetAppCode(), UserID: req.GetUserId(), AssetType: req.GetAssetType(), Page: req.GetPage(), PageSize: req.GetPageSize(), }) if err != nil { return nil, xerr.ToGRPCError(err) } resp := &walletv1.ListWalletTransactionsResponse{Transactions: make([]*walletv1.WalletTransaction, 0, len(items)), Total: total} for _, item := range items { resp.Transactions = append(resp.Transactions, &walletv1.WalletTransaction{ EntryId: item.EntryID, TransactionId: item.TransactionID, BizType: item.BizType, AssetType: item.AssetType, AvailableDelta: item.AvailableDelta, FrozenDelta: item.FrozenDelta, AvailableAfter: item.AvailableAfter, FrozenAfter: item.FrozenAfter, CounterpartyUserId: item.CounterpartyUserID, RoomId: item.RoomID, CreatedAtMs: item.CreatedAtMS, }) } return resp, nil } // ListVipPackages 返回当前 VIP 和可购买包列表。 func (s *Server) ListVipPackages(ctx context.Context, req *walletv1.ListVipPackagesRequest) (*walletv1.ListVipPackagesResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) current, levels, err := s.svc.ListVipPackages(ctx, req.GetUserId()) if err != nil { return nil, xerr.ToGRPCError(err) } resp := &walletv1.ListVipPackagesResponse{CurrentVip: vipToProto(current), Packages: make([]*walletv1.VipLevel, 0, len(levels))} for _, level := range levels { resp.Packages = append(resp.Packages, vipLevelToProto(level)) } return resp, nil } // GetMyVip 返回当前登录用户 VIP 状态。 func (s *Server) GetMyVip(ctx context.Context, req *walletv1.GetMyVipRequest) (*walletv1.GetMyVipResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) vip, err := s.svc.GetMyVip(ctx, req.GetUserId()) if err != nil { return nil, xerr.ToGRPCError(err) } return &walletv1.GetMyVipResponse{Vip: vipToProto(vip)}, nil } // PurchaseVip 购买、续期或升级 VIP。 func (s *Server) PurchaseVip(ctx context.Context, req *walletv1.PurchaseVipRequest) (*walletv1.PurchaseVipResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) receipt, err := s.svc.PurchaseVip(ctx, ledger.PurchaseVipCommand{ AppCode: req.GetAppCode(), CommandID: req.GetCommandId(), UserID: req.GetUserId(), Level: req.GetLevel(), }) if err != nil { return nil, xerr.ToGRPCError(err) } return &walletv1.PurchaseVipResponse{ OrderId: receipt.OrderID, TransactionId: receipt.TransactionID, Vip: vipToProto(receipt.Vip), CoinSpent: receipt.CoinSpent, CoinBalanceAfter: receipt.CoinBalanceAfter, RewardItems: vipRewardItemsToProto(receipt.RewardItems), }, nil } // DebitCPBreakupFee 扣除解除 CP/兄弟/姐妹关系费用;关系状态确认由 user-service 负责。 func (s *Server) DebitCPBreakupFee(ctx context.Context, req *walletv1.DebitCPBreakupFeeRequest) (*walletv1.DebitCPBreakupFeeResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) receipt, err := s.svc.DebitCPBreakupFee(ctx, ledger.CPBreakupFeeCommand{ AppCode: req.GetAppCode(), CommandID: req.GetCommandId(), UserID: req.GetUserId(), RelationshipID: req.GetRelationshipId(), RelationType: req.GetRelationType(), Amount: req.GetAmount(), }) if err != nil { return nil, xerr.ToGRPCError(err) } return &walletv1.DebitCPBreakupFeeResponse{ TransactionId: receipt.TransactionID, CoinSpent: receipt.CoinSpent, CoinBalanceAfter: receipt.CoinBalanceAfter, Balance: balanceToProto(receipt.Balance), }, nil } // DebitWheelDraw 扣除转盘抽奖金币;中奖结果不在 wallet 内生成,避免钱包反向拥有活动规则。 func (s *Server) DebitWheelDraw(ctx context.Context, req *walletv1.DebitWheelDrawRequest) (*walletv1.DebitWheelDrawResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) receipt, err := s.svc.DebitWheelDraw(ctx, ledger.WheelDrawDebitCommand{ AppCode: req.GetAppCode(), CommandID: req.GetCommandId(), UserID: req.GetUserId(), WheelID: req.GetWheelId(), DrawCount: req.GetDrawCount(), Amount: req.GetAmount(), }) if err != nil { return nil, xerr.ToGRPCError(err) } return &walletv1.DebitWheelDrawResponse{ TransactionId: receipt.TransactionID, CoinSpent: receipt.CoinSpent, CoinBalanceAfter: receipt.CoinBalanceAfter, Balance: balanceToProto(receipt.Balance), }, nil } // GrantVip 统一处理活动和后台赠送 VIP。 func (s *Server) GrantVip(ctx context.Context, req *walletv1.GrantVipRequest) (*walletv1.GrantVipResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) receipt, err := s.svc.GrantVip(ctx, ledger.GrantVipCommand{ AppCode: req.GetAppCode(), CommandID: req.GetCommandId(), TargetUserID: req.GetTargetUserId(), Level: req.GetLevel(), GrantSource: req.GetGrantSource(), OperatorUserID: req.GetOperatorUserId(), Reason: req.GetReason(), }) if err != nil { return nil, xerr.ToGRPCError(err) } return &walletv1.GrantVipResponse{ TransactionId: receipt.TransactionID, Vip: vipToProto(receipt.Vip), RewardItems: vipRewardItemsToProto(receipt.RewardItems), ServerTimeMs: time.Now().UnixMilli(), }, nil } // ListAdminVipLevels 返回后台 VIP 配置。 func (s *Server) ListAdminVipLevels(ctx context.Context, req *walletv1.ListAdminVipLevelsRequest) (*walletv1.ListAdminVipLevelsResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) levels, err := s.svc.ListAdminVipLevels(ctx) if err != nil { return nil, xerr.ToGRPCError(err) } resp := &walletv1.ListAdminVipLevelsResponse{Levels: make([]*walletv1.VipLevel, 0, len(levels)), ServerTimeMs: time.Now().UnixMilli()} for _, level := range levels { resp.Levels = append(resp.Levels, vipLevelToProto(level)) } return resp, nil } // UpdateAdminVipLevels 保存后台 VIP 配置。 func (s *Server) UpdateAdminVipLevels(ctx context.Context, req *walletv1.UpdateAdminVipLevelsRequest) (*walletv1.UpdateAdminVipLevelsResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) commands := make([]ledger.AdminVipLevelCommand, 0, len(req.GetLevels())) for _, item := range req.GetLevels() { if item == nil { continue } commands = append(commands, ledger.AdminVipLevelCommand{ Level: item.GetLevel(), Name: item.GetName(), Status: item.GetStatus(), PriceCoin: item.GetPriceCoin(), DurationMS: item.GetDurationMs(), RewardResourceGroupID: item.GetRewardResourceGroupId(), RequiredRechargeCoinAmount: item.GetRequiredRechargeCoinAmount(), SortOrder: item.GetSortOrder(), }) } levels, err := s.svc.UpdateAdminVipLevels(ctx, commands, req.GetOperatorUserId()) if err != nil { return nil, xerr.ToGRPCError(err) } resp := &walletv1.UpdateAdminVipLevelsResponse{Levels: make([]*walletv1.VipLevel, 0, len(levels)), ServerTimeMs: time.Now().UnixMilli()} for _, level := range levels { resp.Levels = append(resp.Levels, vipLevelToProto(level)) } return resp, nil } // AdminCreditAsset 处理后台手动调账,公网权限和审计入口由 gateway/admin 层控制。 func (s *Server) AdminCreditAsset(ctx context.Context, req *walletv1.AdminCreditAssetRequest) (*walletv1.AdminCreditAssetResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) balance, transactionID, err := s.svc.AdminCreditAsset(ctx, ledger.AdminCreditAssetCommand{ AppCode: req.GetAppCode(), CommandID: req.GetCommandId(), TargetUserID: req.GetTargetUserId(), AssetType: req.GetAssetType(), Amount: req.GetAmount(), OperatorUserID: req.GetOperatorUserId(), Reason: req.GetReason(), EvidenceRef: req.GetEvidenceRef(), }) if err != nil { return nil, xerr.ToGRPCError(err) } return &walletv1.AdminCreditAssetResponse{ TransactionId: transactionID, Balance: &walletv1.AssetBalance{ AppCode: balance.AppCode, AssetType: balance.AssetType, AvailableAmount: balance.AvailableAmount, FrozenAmount: balance.FrozenAmount, Version: balance.Version, }, }, nil } func balancesToProto(balances []ledger.AssetBalance) []*walletv1.AssetBalance { items := make([]*walletv1.AssetBalance, 0, len(balances)) for _, balance := range balances { items = append(items, balanceToProto(balance)) } return items } func balanceToProto(balance ledger.AssetBalance) *walletv1.AssetBalance { return &walletv1.AssetBalance{ AppCode: balance.AppCode, AssetType: balance.AssetType, AvailableAmount: balance.AvailableAmount, FrozenAmount: balance.FrozenAmount, Version: balance.Version, } } func giftWallItemToProto(item ledger.GiftWallItem) *walletv1.GiftWallItem { return &walletv1.GiftWallItem{ GiftId: item.GiftID, GiftName: item.GiftName, ResourceId: item.ResourceID, Resource: giftWallResourceToProto(item.Resource), ResourceSnapshotJson: item.ResourceSnapshotJSON, GiftTypeCode: item.GiftTypeCode, PresentationJson: item.PresentationJSON, GiftCount: item.GiftCount, TotalValue: item.TotalValue, TotalCoinValue: item.TotalCoinValue, TotalDiamondValue: item.TotalDiamondValue, TotalGiftPoint: item.TotalGiftPoint, TotalHeatValue: item.TotalHeatValue, ChargeAssetType: item.ChargeAssetType, LastPriceVersion: item.LastPriceVersion, FirstReceivedAtMs: item.FirstReceivedAtMS, LastReceivedAtMs: item.LastReceivedAtMS, SortOrder: item.SortOrder, } } func giftWallResourceToProto(resource ledger.GiftWallResource) *walletv1.Resource { if resource.ResourceID == 0 { return nil } return &walletv1.Resource{ AppCode: resource.AppCode, ResourceId: resource.ResourceID, ResourceCode: resource.ResourceCode, ResourceType: resource.ResourceType, Name: resource.Name, Status: resource.Status, Grantable: resource.Grantable, GrantStrategy: resource.GrantStrategy, WalletAssetType: resource.WalletAssetType, WalletAssetAmount: resource.WalletAssetAmount, UsageScopes: resource.UsageScopes, AssetUrl: resource.AssetURL, PreviewUrl: resource.PreviewURL, AnimationUrl: resource.AnimationURL, MetadataJson: resource.MetadataJSON, SortOrder: resource.SortOrder, CreatedAtMs: resource.CreatedAtMS, UpdatedAtMs: resource.UpdatedAtMS, } } func walletFeatureFlagsToProto(flags ledger.WalletFeatureFlags) *walletv1.WalletFeatureFlags { return &walletv1.WalletFeatureFlags{ RechargeEnabled: flags.RechargeEnabled, DiamondExchangeEnabled: flags.DiamondExchangeEnabled, } } func vipToProto(vip ledger.UserVip) *walletv1.UserVip { return &walletv1.UserVip{ UserId: vip.UserID, Level: vip.Level, Name: vip.Name, Active: vip.Active, StartedAtMs: vip.StartedAtMS, ExpiresAtMs: vip.ExpiresAtMS, UpdatedAtMs: vip.UpdatedAtMS, } } func vipLevelToProto(level ledger.VipLevel) *walletv1.VipLevel { return &walletv1.VipLevel{ Level: level.Level, Name: level.Name, Status: level.Status, PriceCoin: level.PriceCoin, DurationMs: level.DurationMS, RewardResourceGroupId: level.RewardResourceGroupID, RewardItems: vipRewardItemsToProto(level.RewardItems), CanPurchase: level.CanPurchase, SortOrder: level.SortOrder, RechargeGateRequired: level.RechargeGateRequired, RequiredRechargeCoinAmount: level.RequiredRechargeCoinAmount, UserRechargeCoinAmount: level.UserRechargeCoinAmount, PurchaseLockedReason: level.PurchaseLockedReason, CreatedAtMs: level.CreatedAtMS, UpdatedAtMs: level.UpdatedAtMS, } } func vipRewardItemsToProto(items []ledger.VipRewardItem) []*walletv1.VipRewardItem { resp := make([]*walletv1.VipRewardItem, 0, len(items)) for _, item := range items { resp = append(resp, &walletv1.VipRewardItem{ ResourceId: item.ResourceID, ResourceCode: item.ResourceCode, ResourceType: item.ResourceType, Name: item.Name, Quantity: item.Quantity, ExpiresAtMs: item.ExpiresAtMS, AssetUrl: item.AssetURL, PreviewUrl: item.PreviewURL, AnimationUrl: item.AnimationURL, }) } return resp } // CreditTaskReward 处理 activity-service 任务奖励金币入账。 func (s *Server) CreditTaskReward(ctx context.Context, req *walletv1.CreditTaskRewardRequest) (*walletv1.CreditTaskRewardResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) receipt, err := s.svc.CreditTaskReward(ctx, ledger.TaskRewardCommand{ AppCode: req.GetAppCode(), CommandID: req.GetCommandId(), TargetUserID: req.GetTargetUserId(), Amount: req.GetAmount(), TaskType: req.GetTaskType(), TaskID: req.GetTaskId(), CycleKey: req.GetCycleKey(), Reason: req.GetReason(), }) if err != nil { return nil, xerr.ToGRPCError(err) } return &walletv1.CreditTaskRewardResponse{ TransactionId: receipt.TransactionID, Amount: receipt.Amount, GrantedAtMs: receipt.GrantedAtMS, Balance: &walletv1.AssetBalance{ AppCode: receipt.Balance.AppCode, AssetType: receipt.Balance.AssetType, AvailableAmount: receipt.Balance.AvailableAmount, FrozenAmount: receipt.Balance.FrozenAmount, Version: receipt.Balance.Version, }, }, nil } // CreditLuckyGiftReward 处理 activity-service 幸运礼物抽奖 outbox 的中奖金币入账。 func (s *Server) CreditLuckyGiftReward(ctx context.Context, req *walletv1.CreditLuckyGiftRewardRequest) (*walletv1.CreditLuckyGiftRewardResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) receipt, err := s.svc.CreditLuckyGiftReward(ctx, ledger.LuckyGiftRewardCommand{ AppCode: req.GetAppCode(), CommandID: req.GetCommandId(), TargetUserID: req.GetTargetUserId(), Amount: req.GetAmount(), DrawID: req.GetDrawId(), RoomID: req.GetRoomId(), VisibleRegionID: req.GetVisibleRegionId(), GiftID: req.GetGiftId(), PoolID: req.GetPoolId(), Reason: req.GetReason(), }) if err != nil { return nil, xerr.ToGRPCError(err) } return &walletv1.CreditLuckyGiftRewardResponse{ TransactionId: receipt.TransactionID, Amount: receipt.Amount, GrantedAtMs: receipt.GrantedAtMS, Balance: &walletv1.AssetBalance{ AppCode: receipt.Balance.AppCode, AssetType: receipt.Balance.AssetType, AvailableAmount: receipt.Balance.AvailableAmount, FrozenAmount: receipt.Balance.FrozenAmount, Version: receipt.Balance.Version, }, }, nil } // CreditWheelReward 处理 activity-service 转盘金币奖品 outbox 的入账。 func (s *Server) CreditWheelReward(ctx context.Context, req *walletv1.CreditWheelRewardRequest) (*walletv1.CreditWheelRewardResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) receipt, err := s.svc.CreditWheelReward(ctx, ledger.WheelRewardCommand{ AppCode: req.GetAppCode(), CommandID: req.GetCommandId(), TargetUserID: req.GetTargetUserId(), Amount: req.GetAmount(), DrawID: req.GetDrawId(), WheelID: req.GetWheelId(), SelectedTierID: req.GetSelectedTierId(), VisibleRegionID: req.GetVisibleRegionId(), Reason: req.GetReason(), }) if err != nil { return nil, xerr.ToGRPCError(err) } return &walletv1.CreditWheelRewardResponse{ TransactionId: receipt.TransactionID, Amount: receipt.Amount, GrantedAtMs: receipt.GrantedAtMS, Balance: &walletv1.AssetBalance{ AppCode: receipt.Balance.AppCode, AssetType: receipt.Balance.AssetType, AvailableAmount: receipt.Balance.AvailableAmount, FrozenAmount: receipt.Balance.FrozenAmount, Version: receipt.Balance.Version, }, }, nil } // CreditRoomTurnoverReward 处理 activity-service 每周房间流水奖励金币入账。 func (s *Server) CreditRoomTurnoverReward(ctx context.Context, req *walletv1.CreditRoomTurnoverRewardRequest) (*walletv1.CreditRoomTurnoverRewardResponse, error) { // gRPC 层不解释活动规则,只把 activity-service 的结算命令转换为钱包领域命令。 ctx = appcode.WithContext(ctx, req.GetAppCode()) receipt, err := s.svc.CreditRoomTurnoverReward(ctx, ledger.RoomTurnoverRewardCommand{ AppCode: req.GetAppCode(), CommandID: req.GetCommandId(), TargetUserID: req.GetTargetUserId(), Amount: req.GetAmount(), SettlementID: req.GetSettlementId(), RoomID: req.GetRoomId(), PeriodStartMS: req.GetPeriodStartMs(), PeriodEndMS: req.GetPeriodEndMs(), CoinSpent: req.GetCoinSpent(), TierID: req.GetTierId(), TierCode: req.GetTierCode(), Reason: req.GetReason(), }) if err != nil { return nil, xerr.ToGRPCError(err) } return &walletv1.CreditRoomTurnoverRewardResponse{ TransactionId: receipt.TransactionID, Amount: receipt.Amount, GrantedAtMs: receipt.GrantedAtMS, Balance: &walletv1.AssetBalance{ AppCode: receipt.Balance.AppCode, AssetType: receipt.Balance.AssetType, AvailableAmount: receipt.Balance.AvailableAmount, FrozenAmount: receipt.Balance.FrozenAmount, Version: receipt.Balance.Version, }, }, nil } // CreditInviteActivityReward 处理 activity-service 邀请活动奖励金币入账。 func (s *Server) CreditInviteActivityReward(ctx context.Context, req *walletv1.CreditInviteActivityRewardRequest) (*walletv1.CreditInviteActivityRewardResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) receipt, err := s.svc.CreditInviteActivityReward(ctx, ledger.InviteActivityRewardCommand{ AppCode: req.GetAppCode(), CommandID: req.GetCommandId(), TargetUserID: req.GetTargetUserId(), Amount: req.GetAmount(), ClaimID: req.GetClaimId(), RewardType: req.GetRewardType(), TierID: req.GetTierId(), TierCode: req.GetTierCode(), CycleKey: req.GetCycleKey(), ReachedValue: req.GetReachedValue(), Reason: req.GetReason(), }) if err != nil { return nil, xerr.ToGRPCError(err) } return &walletv1.CreditInviteActivityRewardResponse{ TransactionId: receipt.TransactionID, Amount: receipt.Amount, GrantedAtMs: receipt.GrantedAtMS, Balance: &walletv1.AssetBalance{ AppCode: receipt.Balance.AppCode, AssetType: receipt.Balance.AssetType, AvailableAmount: receipt.Balance.AvailableAmount, FrozenAmount: receipt.Balance.FrozenAmount, Version: receipt.Balance.Version, }, }, nil } // CreditAgencyOpeningReward 处理 activity-service 代理开业流水档位奖励金币入账。 func (s *Server) CreditAgencyOpeningReward(ctx context.Context, req *walletv1.CreditAgencyOpeningRewardRequest) (*walletv1.CreditAgencyOpeningRewardResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) receipt, err := s.svc.CreditAgencyOpeningReward(ctx, ledger.AgencyOpeningRewardCommand{ AppCode: req.GetAppCode(), CommandID: req.GetCommandId(), TargetUserID: req.GetTargetUserId(), Amount: req.GetAmount(), ApplicationID: req.GetApplicationId(), CycleID: req.GetCycleId(), AgencyID: req.GetAgencyId(), RankNo: req.GetRankNo(), ScoreCoins: req.GetScoreCoins(), Reason: req.GetReason(), }) if err != nil { return nil, xerr.ToGRPCError(err) } return &walletv1.CreditAgencyOpeningRewardResponse{ TransactionId: receipt.TransactionID, Amount: receipt.Amount, GrantedAtMs: receipt.GrantedAtMS, Balance: &walletv1.AssetBalance{ AppCode: receipt.Balance.AppCode, AssetType: receipt.Balance.AssetType, AvailableAmount: receipt.Balance.AvailableAmount, FrozenAmount: receipt.Balance.FrozenAmount, Version: receipt.Balance.Version, }, }, nil } // ApplyGameCoinChange 处理 game-service 发起的游戏专用金币改账。 func (s *Server) ApplyGameCoinChange(ctx context.Context, req *walletv1.ApplyGameCoinChangeRequest) (*walletv1.ApplyGameCoinChangeResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) receipt, err := s.svc.ApplyGameCoinChange(ctx, ledger.GameCoinChangeCommand{ AppCode: req.GetAppCode(), CommandID: req.GetCommandId(), UserID: req.GetUserId(), PlatformCode: req.GetPlatformCode(), GameID: req.GetGameId(), ProviderOrderID: req.GetProviderOrderId(), ProviderRoundID: req.GetProviderRoundId(), OpType: req.GetOpType(), CoinAmount: req.GetCoinAmount(), RoomID: req.GetRoomId(), RequestHash: req.GetRequestHash(), }) if err != nil { return nil, xerr.ToGRPCError(err) } return &walletv1.ApplyGameCoinChangeResponse{ WalletTransactionId: receipt.TransactionID, BalanceAfter: receipt.BalanceAfter, IdempotentReplay: receipt.IdempotentReplay, }, nil } // AdminCreditCoinSellerStock 处理后台币商进货或金币补偿,币商身份由 admin-server 先行校验。 func (s *Server) AdminCreditCoinSellerStock(ctx context.Context, req *walletv1.AdminCreditCoinSellerStockRequest) (*walletv1.AdminCreditCoinSellerStockResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) receipt, err := s.svc.AdminCreditCoinSellerStock(ctx, ledger.CoinSellerStockCreditCommand{ AppCode: req.GetAppCode(), CommandID: req.GetCommandId(), SellerUserID: req.GetSellerUserId(), SellerCountryID: req.GetSellerCountryId(), SellerRegionID: req.GetSellerRegionId(), StockType: req.GetStockType(), CoinAmount: req.GetCoinAmount(), PaidCurrencyCode: req.GetPaidCurrencyCode(), PaidAmountMicro: req.GetPaidAmountMicro(), PaymentRef: req.GetPaymentRef(), EvidenceRef: req.GetEvidenceRef(), OperatorUserID: req.GetOperatorUserId(), Reason: req.GetReason(), }) if err != nil { return nil, xerr.ToGRPCError(err) } return &walletv1.AdminCreditCoinSellerStockResponse{ TransactionId: receipt.TransactionID, SellerUserId: receipt.SellerUserID, StockType: receipt.StockType, CoinAmount: receipt.CoinAmount, PaidCurrencyCode: receipt.PaidCurrencyCode, PaidAmountMicro: receipt.PaidAmountMicro, CountsAsSellerRecharge: receipt.CountsAsSellerRecharge, BalanceAfter: receipt.BalanceAfter, CreatedAtMs: receipt.CreatedAtMS, }, nil } // TransferCoinFromSeller 处理币商转玩家金币,调用方必须先完成币商身份校验。 func (s *Server) TransferCoinFromSeller(ctx context.Context, req *walletv1.TransferCoinFromSellerRequest) (*walletv1.TransferCoinFromSellerResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) receipt, err := s.svc.TransferCoinFromSeller(ctx, ledger.CoinSellerTransferCommand{ AppCode: req.GetAppCode(), CommandID: req.GetCommandId(), SellerUserID: req.GetSellerUserId(), TargetUserID: req.GetTargetUserId(), TargetCountryID: req.GetTargetCountryId(), SellerRegionID: req.GetSellerRegionId(), TargetRegionID: req.GetTargetRegionId(), Amount: req.GetAmount(), Reason: req.GetReason(), }) if err != nil { return nil, xerr.ToGRPCError(err) } return &walletv1.TransferCoinFromSellerResponse{ TransactionId: receipt.TransactionID, SellerBalanceAfter: receipt.SellerBalanceAfter, TargetBalanceAfter: receipt.TargetBalanceAfter, Amount: receipt.Amount, RechargeUsdMinor: receipt.RechargeUSDMinor, RechargeCurrencyCode: receipt.RechargeCurrencyCode, RechargePolicyId: receipt.RechargePolicyID, RechargePolicyVersion: receipt.RechargePolicyVersion, RechargePolicyCoinAmount: receipt.RechargePolicyCoinAmount, RechargePolicyUsdMinorAmount: receipt.RechargePolicyUSDMinorUnit, }, nil } // ListCoinSellerSalaryExchangeRateTiers 返回工资转币商使用的区域比例区间,调用方用它做预计到账展示。 func (s *Server) ListCoinSellerSalaryExchangeRateTiers(ctx context.Context, req *walletv1.ListCoinSellerSalaryExchangeRateTiersRequest) (*walletv1.ListCoinSellerSalaryExchangeRateTiersResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) tiers, err := s.svc.ListCoinSellerSalaryExchangeRateTiers(ctx, req.GetAppCode(), req.GetRegionId(), req.GetIncludeDisabled()) if err != nil { return nil, xerr.ToGRPCError(err) } response := &walletv1.ListCoinSellerSalaryExchangeRateTiersResponse{Tiers: make([]*walletv1.CoinSellerSalaryExchangeRateTier, 0, len(tiers))} for _, tier := range tiers { response.Tiers = append(response.Tiers, &walletv1.CoinSellerSalaryExchangeRateTier{ RegionId: tier.RegionID, MinUsdMinor: tier.MinUSDMinor, MaxUsdMinor: tier.MaxUSDMinor, CoinPerUsd: tier.CoinPerUSD, Status: tier.Status, SortOrder: int32(tier.SortOrder), UpdatedAtMs: tier.UpdatedAtMS, }) } return response, nil } // ExchangeSalaryToCoin 处理用户工资兑换普通金币,身份资产已经由 gateway 校验并注入。 func (s *Server) ExchangeSalaryToCoin(ctx context.Context, req *walletv1.ExchangeSalaryToCoinRequest) (*walletv1.ExchangeSalaryToCoinResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) receipt, err := s.svc.ExchangeSalaryToCoin(ctx, ledger.SalaryExchangeCommand{ AppCode: req.GetAppCode(), CommandID: req.GetCommandId(), UserID: req.GetUserId(), SalaryAssetType: req.GetSalaryAssetType(), SalaryUSDMinor: req.GetSalaryUsdMinor(), Reason: req.GetReason(), }) if err != nil { return nil, xerr.ToGRPCError(err) } return &walletv1.ExchangeSalaryToCoinResponse{ TransactionId: receipt.TransactionID, SalaryBalanceAfter: receipt.SalaryBalanceAfter, CoinBalanceAfter: receipt.CoinBalanceAfter, SalaryUsdMinor: receipt.SalaryUSDMinor, CoinAmount: receipt.CoinAmount, CoinPerUsd: receipt.CoinPerUSD, }, nil } // TransferSalaryToCoinSeller 处理用户工资转同区域币商专用金币库存。 func (s *Server) TransferSalaryToCoinSeller(ctx context.Context, req *walletv1.TransferSalaryToCoinSellerRequest) (*walletv1.TransferSalaryToCoinSellerResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) receipt, err := s.svc.TransferSalaryToCoinSeller(ctx, ledger.SalaryTransferToCoinSellerCommand{ AppCode: req.GetAppCode(), CommandID: req.GetCommandId(), SourceUserID: req.GetSourceUserId(), SellerUserID: req.GetSellerUserId(), SalaryAssetType: req.GetSalaryAssetType(), SalaryUSDMinor: req.GetSalaryUsdMinor(), RegionID: req.GetRegionId(), Reason: req.GetReason(), }) if err != nil { return nil, xerr.ToGRPCError(err) } return &walletv1.TransferSalaryToCoinSellerResponse{ TransactionId: receipt.TransactionID, SourceSalaryBalanceAfter: receipt.SourceSalaryBalanceAfter, SellerBalanceAfter: receipt.SellerBalanceAfter, SalaryUsdMinor: receipt.SalaryUSDMinor, CoinAmount: receipt.CoinAmount, CoinPerUsd: receipt.CoinPerUSD, RateMinUsdMinor: receipt.RateMinUSDMinor, RateMaxUsdMinor: receipt.RateMaxUSDMinor, }, nil }