package walletapi import ( "hyapp/services/gateway-service/internal/transport/http/httpkit" "net/http" "strconv" "strings" userv1 "hyapp.local/api/proto/user/v1" walletv1 "hyapp.local/api/proto/wallet/v1" "hyapp/pkg/appcode" "hyapp/pkg/xerr" "hyapp/services/gateway-service/internal/auth" ) type assetBalanceData struct { AssetType string `json:"asset_type"` AvailableAmount int64 `json:"available_amount"` FrozenAmount int64 `json:"frozen_amount"` Version int64 `json:"version"` } type coinSellerTransferData struct { TransactionID string `json:"transaction_id"` SellerBalanceAfter int64 `json:"seller_balance_after"` TargetBalanceAfter int64 `json:"target_balance_after"` Amount int64 `json:"amount"` RechargeUSDMinor int64 `json:"recharge_usd_minor"` RechargeCurrencyCode string `json:"recharge_currency_code"` RechargePolicyID int64 `json:"recharge_policy_id"` RechargePolicyVersion string `json:"recharge_policy_version"` RechargePolicyCoinAmount int64 `json:"recharge_policy_coin_amount"` RechargePolicyUSDMinorAmount int64 `json:"recharge_policy_usd_minor_amount"` } type coinSellerTransferRequestBody struct { CommandID string `json:"command_id"` CommandIDCamel string `json:"commandId"` TargetUserID int64 `json:"target_user_id"` TargetUserIDCamel int64 `json:"targetUserId"` TargetDisplayUserID string `json:"target_display_user_id"` TargetDisplayUserIDCamel string `json:"targetDisplayUserId"` Amount int64 `json:"amount"` Reason string `json:"reason"` } func (b coinSellerTransferRequestBody) normalizedCommandID() string { if commandID := strings.TrimSpace(b.CommandID); commandID != "" { return commandID } return strings.TrimSpace(b.CommandIDCamel) } func (b coinSellerTransferRequestBody) normalizedTargetDisplayUserID() string { if displayUserID := strings.TrimSpace(b.TargetDisplayUserID); displayUserID != "" { return displayUserID } if displayUserID := strings.TrimSpace(b.TargetDisplayUserIDCamel); displayUserID != "" { return displayUserID } if b.TargetUserID > 0 { return strconv.FormatInt(b.TargetUserID, 10) } if b.TargetUserIDCamel > 0 { return strconv.FormatInt(b.TargetUserIDCamel, 10) } return "" } // getMyBalances 返回当前登录用户的钱包余额。 func (h *Handler) getMyBalances(writer http.ResponseWriter, request *http.Request) { if h.walletClient == nil { httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") return } resp, err := h.walletClient.GetBalances(request.Context(), &walletv1.GetBalancesRequest{ RequestId: httpkit.RequestIDFromContext(request.Context()), UserId: auth.UserIDFromContext(request.Context()), AssetTypes: walletAssetTypes(request), AppCode: appcode.FromContext(request.Context()), }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } balances := make([]assetBalanceData, 0, len(resp.GetBalances())) for _, balance := range resp.GetBalances() { balances = append(balances, assetBalanceData{ AssetType: balance.GetAssetType(), AvailableAmount: balance.GetAvailableAmount(), FrozenAmount: balance.GetFrozenAmount(), Version: balance.GetVersion(), }) } httpkit.WriteOK(writer, request, map[string]any{"balances": balances}) } // transferCoinFromSeller 处理币商给玩家转金币的 App 入口。 func (h *Handler) transferCoinFromSeller(writer http.ResponseWriter, request *http.Request) { if h.walletClient == nil || h.userHostClient == nil || h.userProfileClient == nil || h.userIdentityClient == nil { httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") return } var body coinSellerTransferRequestBody if !httpkit.Decode(writer, request, &body) { return } commandID := body.normalizedCommandID() targetDisplayUserID := body.normalizedTargetDisplayUserID() reason := strings.TrimSpace(body.Reason) if commandID == "" || targetDisplayUserID == "" || body.Amount <= 0 || reason == "" { httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument") return } sellerUserID := auth.UserIDFromContext(request.Context()) profileResp, err := h.userHostClient.GetCoinSellerProfile(request.Context(), &userv1.GetCoinSellerProfileRequest{ Meta: httpkit.UserMeta(request, ""), UserId: sellerUserID, }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } profile := profileResp.GetCoinSellerProfile() if profile == nil || profile.GetStatus() != "active" || profile.GetMerchantAssetType() != "COIN_SELLER_COIN" { httpkit.WriteError(writer, request, http.StatusForbidden, httpkit.CodePermissionDenied, "permission denied") return } targetUserID, ok := h.resolveCoinSellerTransferTargetUserID(writer, request, targetDisplayUserID) if !ok { return } sellerRegionID, targetRegionID, targetCountryID, ok := h.coinSellerTransferRegions(writer, request, sellerUserID, targetUserID) if !ok { return } resp, err := h.walletClient.TransferCoinFromSeller(request.Context(), &walletv1.TransferCoinFromSellerRequest{ CommandId: commandID, SellerUserId: sellerUserID, TargetUserId: targetUserID, SellerRegionId: sellerRegionID, TargetRegionId: targetRegionID, TargetCountryId: targetCountryID, Amount: body.Amount, Reason: reason, AppCode: appcode.FromContext(request.Context()), }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } httpkit.WriteOK(writer, request, coinSellerTransferData{ TransactionID: resp.GetTransactionId(), SellerBalanceAfter: resp.GetSellerBalanceAfter(), TargetBalanceAfter: resp.GetTargetBalanceAfter(), Amount: resp.GetAmount(), RechargeUSDMinor: resp.GetRechargeUsdMinor(), RechargeCurrencyCode: resp.GetRechargeCurrencyCode(), RechargePolicyID: resp.GetRechargePolicyId(), RechargePolicyVersion: resp.GetRechargePolicyVersion(), RechargePolicyCoinAmount: resp.GetRechargePolicyCoinAmount(), RechargePolicyUSDMinorAmount: resp.GetRechargePolicyUsdMinorAmount(), }) } func (h *Handler) resolveCoinSellerTransferTargetUserID(writer http.ResponseWriter, request *http.Request, targetDisplayUserID string) (int64, bool) { resp, err := h.userIdentityClient.ResolveDisplayUserID(request.Context(), &userv1.ResolveDisplayUserIDRequest{ Meta: httpkit.UserMeta(request, ""), DisplayUserId: targetDisplayUserID, }) if err != nil { httpkit.WriteRPCError(writer, request, err) return 0, false } identity := resp.GetIdentity() targetUserID := identity.GetUserId() if targetUserID <= 0 { httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument") return 0, false } return targetUserID, true } func (h *Handler) coinSellerTransferRegions(writer http.ResponseWriter, request *http.Request, sellerUserID int64, targetUserID int64) (int64, int64, int64, bool) { sellerResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{ Meta: httpkit.UserMeta(request, ""), UserId: sellerUserID, }) if err != nil { httpkit.WriteRPCError(writer, request, err) return 0, 0, 0, false } seller := sellerResp.GetUser() sellerCountryID := seller.GetCountryId() sellerRegionID := seller.GetRegionId() if sellerUserID == targetUserID { if sellerCountryID <= 0 || sellerRegionID <= 0 { httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument") return 0, 0, 0, false } return sellerRegionID, sellerRegionID, sellerCountryID, true } targetResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{ Meta: httpkit.UserMeta(request, ""), UserId: targetUserID, }) if err != nil { httpkit.WriteRPCError(writer, request, err) return 0, 0, 0, false } target := targetResp.GetUser() targetCountryID := target.GetCountryId() targetRegionID := target.GetRegionId() if sellerRegionID <= 0 || targetCountryID <= 0 || targetRegionID <= 0 { httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument") return 0, 0, 0, false } if sellerRegionID != targetRegionID { // 充值政策当前按单一区域生效;跨区转账必须先设计明确的结算区域,不能静默套用任意一方政策。 httpkit.WriteError(writer, request, http.StatusConflict, string(xerr.Conflict), "seller and target are in different regions") return 0, 0, 0, false } return sellerRegionID, targetRegionID, targetCountryID, true } func walletAssetTypes(request *http.Request) []string { rawValues := request.URL.Query()["asset_type"] if len(rawValues) == 0 { rawValues = request.URL.Query()["asset_types"] } assetTypes := make([]string, 0, len(rawValues)) for _, raw := range rawValues { for value := range strings.SplitSeq(raw, ",") { value = strings.TrimSpace(value) if value != "" { assetTypes = append(assetTypes, value) } } } if len(assetTypes) == 0 { return []string{"COIN"} } return assetTypes }