2026-06-18 17:13:56 +08:00

1366 lines
55 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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_urlUSDT 只返回共享收款地址。
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
}