2026-05-25 23:20:32 +08:00

1253 lines
41 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 integration
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"sync"
"time"
"chatapp3-golang/internal/config"
)
type Client struct {
cfg config.Config
httpClient *http.Client
regionConfigCacheMu sync.Mutex
regionConfigCacheMap map[string]regionConfigCacheEntry
}
type UserCredential struct {
UserID Int64Value `json:"userId"`
SysOrigin string `json:"sysOrigin"`
ExpireTime Int64Value `json:"expireTime"`
ReleaseTime Int64Value `json:"releaseTime"`
Version string `json:"version"`
Sign string `json:"sign"`
}
type ConsoleAccount struct {
ID Int64Value `json:"id"`
LoginName string `json:"loginName"`
Nickname string `json:"nickname"`
Token string `json:"token"`
}
type UserProfile struct {
ID Int64Value `json:"id"`
Account string `json:"account"`
UserAvatar string `json:"userAvatar"`
UserNickname string `json:"userNickname"`
CountryID Int64Value `json:"countryId"`
CountryCode string `json:"countryCode"`
CountryName string `json:"countryName"`
RegionCode string `json:"regionCode"`
OriginSys string `json:"originSys"`
AccountStatus string `json:"accountStatus"`
}
type UserRegion struct {
RegionID string `json:"regionId"`
RegionCode string `json:"regionCode"`
}
type RegionConfig struct {
RegionCode string `json:"regionCode"`
SysOrigin string `json:"sysOrigin"`
RegionName string `json:"regionName"`
CountryCodes string `json:"countryCodes"`
Del bool `json:"del"`
}
type regionConfigCacheEntry struct {
expiresAt time.Time
items []RegionConfig
}
const regionConfigCacheTTL = time.Minute
type RoomContributionActivityCount struct {
ID string `json:"id"`
RoomID Int64Value `json:"roomId"`
ContributionValue AmountValue `json:"contributionValue"`
DateNumber int `json:"dateNumber"`
Received bool `json:"received"`
RewardCoinsSent bool `json:"rewardCoinsSent"`
}
type RoomProfile struct {
ID Int64Value `json:"id"`
RoomAccount string `json:"roomAccount"`
UserID Int64Value `json:"userId"`
RoomCover string `json:"roomCover"`
RoomName string `json:"roomName"`
SysOrigin string `json:"sysOrigin"`
CountryCode string `json:"countryCode"`
CountryName string `json:"countryName"`
}
type GoldReceiptCommand struct {
ReceiptType string `json:"receiptType"`
UserID int64 `json:"userId"`
SysOrigin string `json:"sysOrigin"`
EventID string `json:"eventId"`
Remark string `json:"remark,omitempty"`
Amount PennyAmountPayload `json:"amount"`
CloseDelayAsset bool `json:"closeDelayAsset"`
OpUserType string `json:"opUserType"`
Origin string `json:"origin,omitempty"`
CustomizeOrigin string `json:"customizeOrigin,omitempty"`
CustomizeOriginDesc string `json:"customizeOriginDesc,omitempty"`
}
type PennyAmountPayload struct {
PennyAmount int64 `json:"pennyAmount,string"`
DollarAmount int64 `json:"dollarAmount"`
}
type TestGoldReceiptCommand struct {
ReceiptType string `json:"receiptType"`
UserID int64 `json:"userId"`
EventType string `json:"eventType"`
EventDesc string `json:"eventDesc"`
EventID string `json:"eventId"`
SysOrigin string `json:"sysOrigin"`
Remark string `json:"remark,omitempty"`
Amount int64 `json:"amount"`
CloseDelayAsset bool `json:"closeDelayAsset"`
}
type InviteCode struct {
UserID Int64Value `json:"userId"`
InviteCode string `json:"inviteCode"`
HasBound bool `json:"hasBound"`
}
type GrantGoldRequest struct {
UserID int64 `json:"userId"`
SysOrigin string `json:"sysOrigin"`
EventID string `json:"eventId"`
Origin string `json:"origin"`
GoldAmount int64 `json:"goldAmount"`
Remark string `json:"remark"`
}
type GrantPropsRequest struct {
TrackID int64 `json:"trackId"`
AcceptUserID int64 `json:"acceptUserId"`
SysOrigin string `json:"sysOrigin"`
Origin string `json:"origin"`
SourceGroupID int64 `json:"sourceGroupId"`
}
type GivePropsBackpackRequest struct {
AcceptUserID int64 `json:"acceptUserId"`
PropsID int64 `json:"propsId"`
Type string `json:"type"`
Origin string `json:"origin"`
OriginDesc string `json:"originDesc"`
Days int `json:"days"`
UseProps bool `json:"useProps"`
AllowGive bool `json:"allowGive"`
OpUser int64 `json:"opUser,omitempty"`
}
type PropsNobleVIPAbility struct {
ID Int64Value `json:"id"`
VIPType string `json:"vipType"`
VIPLevel int `json:"vipLevel"`
AdminNumber int `json:"adminNumber"`
RoomMaxMember int `json:"roomMaxMember"`
AvatarFrameID Int64Value `json:"avatarFrameId"`
CarID Int64Value `json:"carId"`
ChatBubbleID Int64Value `json:"chatBubbleId"`
DataCardID Int64Value `json:"dataCardId"`
}
type PunishAccountRequest struct {
BeOptUserID int64 `json:"beOptUserId"`
OptUserID int64 `json:"optUserId"`
Status string `json:"status"`
Description string `json:"description"`
}
type UnblockAccountRequest struct {
BeOptUserID int64 `json:"beOptUserId"`
OptUserID int64 `json:"optUserId"`
Description string `json:"description"`
}
type SendActivityRewardRequest struct {
TrackID int64 `json:"trackId"`
Origin string `json:"origin"`
SysOrigin string `json:"sysOrigin"`
SourceGroupID int64 `json:"sourceGroupId"`
AcceptUserID int64 `json:"acceptUserId"`
}
type TeamPolicyReleaseQuery struct {
SysOrigin string
Region string
PolicyType string
CountryCode string
}
type TeamPolicyRelease struct {
ID Int64Value `json:"id"`
SysOrigin string `json:"sysOrigin"`
Region string `json:"region"`
CountryCode string `json:"countryCode"`
Type string `json:"type"`
HistoryRelease bool `json:"historyRelease"`
Release bool `json:"release"`
Title string `json:"title"`
Policy []TeamPolicyItem `json:"policy"`
PolicyType string `json:"policyType"`
}
type TeamPolicyItem struct {
Level int `json:"level"`
OnlineTime Int64Value `json:"onlineTime"`
EffectiveDay int `json:"effectiveDay"`
Target Int64Value `json:"target"`
MemberSalary DecimalString `json:"memberSalary"`
OwnSalary DecimalString `json:"ownSalary"`
TotalSalary DecimalString `json:"totalSalary"`
PropsRewards any `json:"propsRewards"`
}
// OfficialNoticeCustomizeRequest 表示发送自定义官方通知的请求体。
type OfficialNoticeCustomizeRequest struct {
ToAccounts []int64 `json:"toAccounts"`
LangCode string `json:"langCode,omitempty"`
NoticeType string `json:"noticeType"`
Expand any `json:"expand,omitempty"`
Title string `json:"title"`
Content string `json:"content"`
}
type RewardGroupItem struct {
ID Int64Value `json:"id"`
Type string `json:"type"`
DetailType string `json:"detailType"`
Name string `json:"name"`
Content string `json:"content"`
Quantity Int64Value `json:"quantity"`
Cover string `json:"cover"`
SourceURL string `json:"sourceUrl"`
Remark string `json:"remark"`
}
type RewardGroupDetail struct {
ID Int64Value `json:"id"`
Name string `json:"name"`
SysOrigin string `json:"sysOrigin"`
RewardConfigList []RewardGroupItem `json:"rewardConfigList"`
}
type UserTotalRecharge struct {
ID Int64Value `json:"id"`
UserID Int64Value `json:"userId"`
Type string `json:"type"`
Amount DecimalString `json:"amount"`
CreateTime StringValue `json:"createTime"`
UpdateTime StringValue `json:"updateTime"`
}
type FreightSellerPage struct {
Records []FreightSellerRecord `json:"records"`
Total int64 `json:"total"`
Current int `json:"current"`
Size int `json:"size"`
}
type FreightSellerRecord struct {
ID Int64Value `json:"id"`
SysOrigin string `json:"sysOrigin"`
UserID Int64Value `json:"userId"`
EarnPoints DecimalString `json:"earnPoints"`
ConsumptionPoints DecimalString `json:"consumptionPoints"`
Balance DecimalString `json:"balance"`
Close bool `json:"close"`
Display bool `json:"display"`
Dealer bool `json:"dealer"`
SuperDealer bool `json:"superDealer"`
SupportedCountries string `json:"supportedCountries"`
ContactInfo string `json:"contactInfo"`
H5Display bool `json:"h5Display"`
SellerQuantity int `json:"sellerQuantity"`
RealSellerQuantity int64 `json:"realSellerQuantity"`
BecomeDays int `json:"becomeDays"`
TransactionCount int64 `json:"transactionCount"`
IsFollow bool `json:"isFollow"`
UserBaseInfo FreightSellerProfile `json:"userBaseInfo"`
NationalFlag []string `json:"nationalFlag"`
OwnNationalFlag string `json:"ownNationalFlag"`
}
type FreightSellerProfile struct {
ID Int64Value `json:"id"`
Account string `json:"account"`
UserAvatar string `json:"userAvatar"`
UserNickname string `json:"userNickname"`
CountryCode string `json:"countryCode"`
CountryName string `json:"countryName"`
RegionCode string `json:"regionCode"`
OriginSys string `json:"originSys"`
}
type FreightBalanceChangeRequest struct {
SysOrigin string `json:"sysOrigin"`
UserID int64 `json:"userId"`
AcceptUserID int64 `json:"acceptUserId"`
Type string `json:"type"`
AcceptAmount string `json:"acceptAmount"`
OrderAmount string `json:"orderAmount"`
USDAmount string `json:"usdAmount"`
Origin string `json:"origin"`
Remark string `json:"remark"`
RechargeType string `json:"rechargeType"`
CreateUser int64 `json:"createUser"`
}
type FreightAgentReviewRequest struct {
SysOrigin string `json:"sysOrigin"`
UserID int64 `json:"userId"`
}
type freightSellerPageResponse struct {
Status *bool `json:"status"`
ErrorMsg string `json:"errorMsg"`
Message string `json:"message"`
Body *FreightSellerPage `json:"body"`
Data *FreightSellerPage `json:"data"`
FreightSellerPage
}
type resultResponse[T any] struct {
Code int `json:"code"`
Message string `json:"message"`
Body T `json:"body"`
Success *bool `json:"success"`
}
type Int64Value int64
func (v *Int64Value) UnmarshalJSON(data []byte) error {
raw := strings.TrimSpace(string(data))
if raw == "" || raw == "null" {
*v = 0
return nil
}
if strings.HasPrefix(raw, "\"") && strings.HasSuffix(raw, "\"") {
raw = strings.Trim(raw, "\"")
}
parsed, err := strconv.ParseInt(raw, 10, 64)
if err != nil {
return err
}
*v = Int64Value(parsed)
return nil
}
type AmountValue int64
func (v *AmountValue) UnmarshalJSON(data []byte) error {
raw := strings.TrimSpace(string(data))
if raw == "" || raw == "null" {
*v = 0
return nil
}
if strings.HasPrefix(raw, "\"") && strings.HasSuffix(raw, "\"") {
raw = strings.Trim(raw, "\"")
}
if raw == "" {
*v = 0
return nil
}
floatRaw := raw
if dot := strings.Index(raw, "."); dot >= 0 {
raw = raw[:dot]
}
if parsed, err := strconv.ParseInt(raw, 10, 64); err == nil {
*v = AmountValue(parsed)
return nil
}
parsed, err := strconv.ParseFloat(floatRaw, 64)
if err != nil {
return err
}
*v = AmountValue(int64(parsed))
return nil
}
type DecimalString string
func (v *DecimalString) UnmarshalJSON(data []byte) error {
raw := strings.TrimSpace(string(data))
if raw == "" || raw == "null" {
*v = ""
return nil
}
if strings.HasPrefix(raw, "\"") && strings.HasSuffix(raw, "\"") {
var parsed string
if err := json.Unmarshal(data, &parsed); err != nil {
return err
}
*v = DecimalString(parsed)
return nil
}
*v = DecimalString(raw)
return nil
}
type StringValue string
func (v *StringValue) UnmarshalJSON(data []byte) error {
raw := strings.TrimSpace(string(data))
if raw == "" || raw == "null" {
*v = ""
return nil
}
if strings.HasPrefix(raw, "\"") && strings.HasSuffix(raw, "\"") {
var parsed string
if err := json.Unmarshal(data, &parsed); err != nil {
return err
}
*v = StringValue(parsed)
return nil
}
*v = StringValue(raw)
return nil
}
func New(cfg config.Config) *Client {
return &Client{
cfg: cfg,
httpClient: &http.Client{
Timeout: cfg.HTTP.Timeout,
},
regionConfigCacheMap: map[string]regionConfigCacheEntry{},
}
}
func (c *Client) AuthenticateToken(ctx context.Context, token string) (UserCredential, error) {
tokenCredential, err := parseUserCredentialToken(token)
if err != nil {
return UserCredential{}, err
}
endpoint := c.cfg.Java.AuthBaseURL + "/auth/client/getUserCredential?userId=" +
url.QueryEscape(strconv.FormatInt(int64(tokenCredential.UserID), 10))
var resp resultResponse[UserCredential]
if err := c.getJSON(ctx, endpoint, nil, &resp); err != nil {
return UserCredential{}, err
}
if int64(resp.Body.UserID) == 0 {
return UserCredential{}, fmt.Errorf("empty credential response")
}
if strings.TrimSpace(resp.Body.Sign) == "" {
return UserCredential{}, fmt.Errorf("empty credential sign")
}
if !strings.EqualFold(strings.TrimSpace(resp.Body.Sign), strings.TrimSpace(tokenCredential.Sign)) {
return UserCredential{}, fmt.Errorf("credential sign mismatch")
}
if strings.TrimSpace(resp.Body.SysOrigin) != "" && !strings.EqualFold(resp.Body.SysOrigin, tokenCredential.SysOrigin) {
return UserCredential{}, fmt.Errorf("credential sysOrigin mismatch")
}
return tokenCredential, nil
}
func (c *Client) CreateIfAbsentUserCredentialToken(ctx context.Context, userID int64) (string, error) {
endpoint := c.cfg.Java.AuthBaseURL + "/auth/client/createIfAbsentUserCredentialToken?userId=" +
url.QueryEscape(strconv.FormatInt(userID, 10))
var resp resultResponse[string]
if err := c.getJSON(ctx, endpoint, nil, &resp); err != nil {
return "", err
}
if strings.TrimSpace(resp.Body) == "" {
return "", fmt.Errorf("empty token response")
}
return strings.TrimSpace(resp.Body), nil
}
// AuthenticateConsoleToken 校验后台 console Bearer token并返回管理员基础信息。
func (c *Client) AuthenticateConsoleToken(ctx context.Context, authorization string) (ConsoleAccount, error) {
authorization = strings.TrimSpace(authorization)
if authorization == "" {
return ConsoleAccount{}, fmt.Errorf("authorization is blank")
}
endpoint := strings.TrimRight(c.cfg.Java.ConsoleBaseURL, "/") + "/account/info"
headers := javaConsoleHeaders(authorization)
var resp resultResponse[ConsoleAccount]
if err := c.getJSON(ctx, endpoint, headers, &resp); err != nil {
return ConsoleAccount{}, err
}
if int64(resp.Body.ID) == 0 {
return ConsoleAccount{}, fmt.Errorf("empty console account response")
}
return resp.Body, nil
}
func javaConsoleHeaders(authorization string) http.Header {
headers := http.Header{}
headers.Set("Authorization", strings.TrimSpace(authorization))
headers.Set("Req-Sys-Origin", "origin=ops;originChild=ops;")
headers.Set("Req-Client", "Ops")
headers.Set("Req-App-Intel", "version=1.0.0;build=1;channel=ops;")
headers.Set("Req-Version", "v2")
headers.Set("Req-Lang", "zh-CN")
headers.Set("Req-Zone", "Asia/Shanghai")
return headers
}
func parseUserCredentialToken(token string) (UserCredential, error) {
token = strings.TrimSpace(token)
if token == "" {
return UserCredential{}, fmt.Errorf("token is blank")
}
parts := strings.SplitN(token, ".", 2)
if len(parts) != 2 {
return UserCredential{}, fmt.Errorf("invalid token format")
}
sign := strings.TrimSpace(parts[0])
if sign == "" {
return UserCredential{}, fmt.Errorf("invalid token sign")
}
decoded, err := base64.StdEncoding.DecodeString(parts[1])
if err != nil {
return UserCredential{}, fmt.Errorf("decode token payload: %w", err)
}
payload, err := url.QueryUnescape(string(decoded))
if err != nil {
return UserCredential{}, fmt.Errorf("unescape token payload: %w", err)
}
items := strings.Split(payload, ":")
if len(items) != 5 {
return UserCredential{}, fmt.Errorf("invalid token payload")
}
userID, err := strconv.ParseInt(strings.TrimSpace(items[1]), 10, 64)
if err != nil {
return UserCredential{}, fmt.Errorf("invalid token userId: %w", err)
}
expireTime, err := strconv.ParseInt(strings.TrimSpace(items[3]), 10, 64)
if err != nil {
return UserCredential{}, fmt.Errorf("invalid token expireTime: %w", err)
}
releaseTime, err := strconv.ParseInt(strings.TrimSpace(items[4]), 10, 64)
if err != nil {
return UserCredential{}, fmt.Errorf("invalid token releaseTime: %w", err)
}
if expireTime <= time.Now().UnixMilli() {
return UserCredential{}, fmt.Errorf("token expired")
}
return UserCredential{
UserID: Int64Value(userID),
SysOrigin: strings.TrimSpace(items[2]),
ExpireTime: Int64Value(expireTime),
ReleaseTime: Int64Value(releaseTime),
Version: strings.TrimSpace(items[0]),
Sign: sign,
}, nil
}
func (c *Client) GetDeviceFingerprint(ctx context.Context, userID int64) (string, error) {
endpoint := c.cfg.Java.DeviceBaseURL + "/user/device/fingerprint?userId=" + url.QueryEscape(strconv.FormatInt(userID, 10))
var resp resultResponse[string]
if err := c.getJSON(ctx, endpoint, nil, &resp); err != nil {
return "", err
}
return strings.TrimSpace(resp.Body), nil
}
func (c *Client) EnsureInviteCode(ctx context.Context, authorization string) (InviteCode, error) {
endpoint := c.cfg.Java.AppBaseURL + "/activity/invite/user/my/invite/code"
headers := javaAppHeadersFromAuthorization(authorization)
if authorization != "" {
headers.Set("Authorization", authorization)
}
var resp resultResponse[InviteCode]
if err := c.getJSON(ctx, endpoint, headers, &resp); err != nil {
return InviteCode{}, err
}
if strings.TrimSpace(resp.Body.InviteCode) == "" {
return InviteCode{}, fmt.Errorf("invite code response is empty")
}
return resp.Body, nil
}
func javaAppHeadersFromAuthorization(authorization string) http.Header {
headers := http.Header{}
token := trimBearerToken(authorization)
sysOrigin := "LIKEI"
var userID int64
if credential, err := parseUserCredentialToken(token); err == nil {
userID = int64(credential.UserID)
if strings.TrimSpace(credential.SysOrigin) != "" {
sysOrigin = strings.TrimSpace(credential.SysOrigin)
}
}
headers.Set("Req-Sys-Origin", "origin="+sysOrigin+";originChild="+sysOrigin)
headers.Set("Req-Client", "Android")
headers.Set("Req-App-Intel", "version=1.0.0;build=100;channel=local;model=go-service;sysVersion=go")
headers.Set("Req-Zone", "Asia/Shanghai")
headers.Set("Req-Imei", "go-service")
if userID > 0 {
headers.Set("Req-Inner-Internal", fmt.Sprintf("time=%d;uid=%d;", time.Now().UnixMilli(), userID))
} else {
headers.Set("Req-Inner-Internal", fmt.Sprintf("time=%d;", time.Now().UnixMilli()))
}
return headers
}
func trimBearerToken(authorization string) string {
token := strings.TrimSpace(authorization)
if len(token) >= 7 && strings.EqualFold(token[:7], "Bearer ") {
return strings.TrimSpace(token[7:])
}
return token
}
func (c *Client) GrantGold(ctx context.Context, req GrantGoldRequest) error {
return c.postJSON(ctx, c.rewardBridgeBaseURL()+"/internal/resident-activity/invite/grant-gold", req, nil, nil)
}
func (c *Client) GrantProps(ctx context.Context, req GrantPropsRequest) error {
return c.postJSON(ctx, c.rewardBridgeBaseURL()+"/internal/resident-activity/invite/grant-props", req, nil, nil)
}
func (c *Client) SendActivityReward(ctx context.Context, req SendActivityRewardRequest) error {
return c.postJSON(ctx, c.cfg.Java.OtherBaseURL+"/props-activity-cnf/client/sendActivityReward", req, nil, nil)
}
func (c *Client) GivePropsBackpack(ctx context.Context, req GivePropsBackpackRequest) error {
return c.postJSON(ctx, c.cfg.Java.OtherBaseURL+"/props/client/giveProps", req, nil, nil)
}
func (c *Client) IncrGiftBackpack(ctx context.Context, userID int64, giftID int64, quantity int) error {
endpoint := c.cfg.Java.OtherBaseURL + "/gift-backpack/client/incrGift?userId=" +
url.QueryEscape(strconv.FormatInt(userID, 10)) +
"&giftId=" + url.QueryEscape(strconv.FormatInt(giftID, 10)) +
"&quantity=" + url.QueryEscape(strconv.Itoa(quantity))
return c.getJSON(ctx, endpoint, nil, nil)
}
func (c *Client) GetNobleVIPAbility(ctx context.Context, sourceID int64) (PropsNobleVIPAbility, error) {
endpoint := c.cfg.Java.OtherBaseURL + "/props/noble-vip/client/getAbilityDTO?sourceId=" +
url.QueryEscape(strconv.FormatInt(sourceID, 10))
var resp resultResponse[PropsNobleVIPAbility]
if err := c.getJSON(ctx, endpoint, nil, &resp); err != nil {
return PropsNobleVIPAbility{}, err
}
return resp.Body, nil
}
func (c *Client) GetUserMaxNobleVIPAbility(ctx context.Context, userID int64) (PropsNobleVIPAbility, error) {
endpoint := c.cfg.Java.OtherBaseURL + "/props/noble-vip/client/getUserMaxAbilityDTO?userId=" +
url.QueryEscape(strconv.FormatInt(userID, 10))
var resp resultResponse[PropsNobleVIPAbility]
if err := c.getJSON(ctx, endpoint, nil, &resp); err != nil {
return PropsNobleVIPAbility{}, err
}
return resp.Body, nil
}
func (c *Client) SwitchUseProps(ctx context.Context, userID int64, propsID int64) error {
endpoint := c.cfg.Java.OtherBaseURL + "/props/client/switch/use-props?userId=" +
url.QueryEscape(strconv.FormatInt(userID, 10)) +
"&propsId=" + url.QueryEscape(strconv.FormatInt(propsID, 10))
return c.getJSON(ctx, endpoint, nil, nil)
}
func (c *Client) ActivateTemporaryBadge(ctx context.Context, userID int64, badgeID int64, days int) error {
endpoint := c.cfg.Java.OtherBaseURL + "/user-badge/client/activationTemporaryAndUse?userId=" +
url.QueryEscape(strconv.FormatInt(userID, 10)) +
"&badgeId=" + url.QueryEscape(strconv.FormatInt(badgeID, 10)) +
"&days=" + url.QueryEscape(strconv.Itoa(days))
var resp resultResponse[bool]
if err := c.getJSON(ctx, endpoint, nil, &resp); err != nil {
return err
}
if !resp.Body {
return fmt.Errorf("activate temporary badge returned false")
}
return nil
}
// SendOfficialNoticeCustomize 调用 external 服务发送自定义官方通知。
func (c *Client) SendOfficialNoticeCustomize(ctx context.Context, req OfficialNoticeCustomizeRequest) error {
return c.postJSON(ctx, c.cfg.Java.ExternalBaseURL+"/official-notice/client/send/customize/template", req, nil, nil)
}
func (c *Client) GetRewardGroupDetail(ctx context.Context, groupID int64) (RewardGroupDetail, error) {
endpoint := c.cfg.Java.OtherBaseURL + "/props-activity/reward/group/client/getByGroupId?id=" + url.QueryEscape(strconv.FormatInt(groupID, 10))
var resp resultResponse[RewardGroupDetail]
if err := c.getJSON(ctx, endpoint, nil, &resp); err != nil {
return RewardGroupDetail{}, err
}
if int64(resp.Body.ID) == 0 {
return RewardGroupDetail{}, fmt.Errorf("empty reward group response")
}
return resp.Body, nil
}
func (c *Client) MapUserTotalRecharge(ctx context.Context, userIDs []int64) (map[int64][]UserTotalRecharge, error) {
if len(userIDs) == 0 {
return map[int64][]UserTotalRecharge{}, nil
}
endpoint := c.cfg.Java.OrderBaseURL + "/total/recharge/client/mapGroup"
var resp resultResponse[map[string][]UserTotalRecharge]
if err := c.postJSON(ctx, endpoint, userIDs, nil, &resp); err != nil {
return nil, err
}
result := make(map[int64][]UserTotalRecharge, len(resp.Body))
for key, value := range resp.Body {
userID, err := strconv.ParseInt(strings.TrimSpace(key), 10, 64)
if err != nil {
continue
}
result[userID] = value
}
return result, nil
}
func (c *Client) GetThisMonthTotalPersonalRecharge(ctx context.Context, userID int64) (DecimalString, error) {
if userID <= 0 {
return DecimalString("0"), nil
}
endpoint := c.cfg.Java.OrderBaseURL + "/user-recharge-count/client/getThisMonthTotalPersonalRecharge?userId=" +
url.QueryEscape(strconv.FormatInt(userID, 10))
var resp resultResponse[DecimalString]
if err := c.getJSON(ctx, endpoint, nil, &resp); err != nil {
return "", err
}
if strings.TrimSpace(string(resp.Body)) == "" {
return DecimalString("0"), nil
}
return resp.Body, nil
}
func (c *Client) PageFreightSellers(ctx context.Context, authorization string, sysOrigin string, cursor int, limit int) (FreightSellerPage, error) {
values := url.Values{}
values.Set("sysOrigin", strings.TrimSpace(sysOrigin))
values.Set("dealer", "true")
values.Set("cursor", strconv.Itoa(cursor))
values.Set("limit", strconv.Itoa(limit))
endpoint := strings.TrimRight(c.cfg.Java.OtherBaseURL, "/") + "/user/freight-balance/page?" + values.Encode()
headers := javaAppHeadersFromAuthorization(authorization)
if strings.TrimSpace(authorization) != "" {
headers.Set("Authorization", strings.TrimSpace(authorization))
}
var resp freightSellerPageResponse
if err := c.getJSON(ctx, endpoint, headers, &resp); err != nil {
return FreightSellerPage{}, err
}
if resp.Status != nil && !*resp.Status {
message := strings.TrimSpace(resp.ErrorMsg)
if message == "" {
message = strings.TrimSpace(resp.Message)
}
if message == "" {
message = "freight seller page failed"
}
return FreightSellerPage{}, errors.New(message)
}
if resp.Body != nil {
return *resp.Body, nil
}
if resp.Data != nil {
return *resp.Data, nil
}
return resp.FreightSellerPage, nil
}
func (c *Client) ExistsFreightBalance(ctx context.Context, userID int64) (bool, error) {
if userID <= 0 {
return false, nil
}
values := url.Values{}
values.Set("userId", strconv.FormatInt(userID, 10))
endpoint := strings.TrimRight(c.cfg.Java.WalletBaseURL, "/") + "/freight-gold/client/existsBalance?" + values.Encode()
var resp resultResponse[bool]
if err := c.getJSON(ctx, endpoint, nil, &resp); err != nil {
return false, err
}
if resp.Success != nil && !*resp.Success {
return false, errors.New(strings.TrimSpace(resp.Message))
}
return resp.Body, nil
}
func (c *Client) ExistsFreightSeller(ctx context.Context, userID int64) (bool, error) {
if userID <= 0 {
return false, nil
}
values := url.Values{}
values.Set("userId", strconv.FormatInt(userID, 10))
endpoint := strings.TrimRight(c.cfg.Java.WalletBaseURL, "/") + "/freight-gold/client/existsSeller?" + values.Encode()
var resp resultResponse[bool]
if err := c.getJSON(ctx, endpoint, nil, &resp); err != nil {
return false, err
}
if resp.Success != nil && !*resp.Success {
return false, errors.New(strings.TrimSpace(resp.Message))
}
return resp.Body, nil
}
func (c *Client) ChangeFreightBalance(ctx context.Context, req FreightBalanceChangeRequest) error {
endpoint := strings.TrimRight(c.cfg.Java.WalletBaseURL, "/") + "/freight-gold/client/changeBalance"
var resp resultResponse[any]
if err := c.postJSON(ctx, endpoint, req, nil, &resp); err != nil {
return err
}
if resp.Success != nil && !*resp.Success {
message := strings.TrimSpace(resp.Message)
if message == "" {
message = "freight balance change failed"
}
return errors.New(message)
}
return nil
}
func (c *Client) AddFreightAgentReview(ctx context.Context, req FreightAgentReviewRequest) error {
endpoint := strings.TrimRight(c.cfg.Java.WalletBaseURL, "/") + "/freight-gold/client/addFreightAgentReview"
var resp resultResponse[any]
if err := c.postJSON(ctx, endpoint, req, nil, &resp); err != nil {
return err
}
if resp.Success != nil && !*resp.Success {
message := strings.TrimSpace(resp.Message)
if message == "" {
message = "freight agent review add failed"
}
return errors.New(message)
}
return nil
}
func (c *Client) IncreaseFreightRechargeRecord(ctx context.Context, userID int64, amount string) error {
if userID <= 0 {
return fmt.Errorf("userId is required")
}
values := url.Values{}
values.Set("userId", strconv.FormatInt(userID, 10))
values.Set("amount", strings.TrimSpace(amount))
endpoint := strings.TrimRight(c.cfg.Java.OrderBaseURL, "/") + "/user/freight/recharge-record/client/inrNowMonthAmount?" + values.Encode()
var resp resultResponse[DecimalString]
if err := c.getJSON(ctx, endpoint, nil, &resp); err != nil {
return err
}
if resp.Success != nil && !*resp.Success {
message := strings.TrimSpace(resp.Message)
if message == "" {
message = "freight recharge record increase failed"
}
return errors.New(message)
}
return nil
}
func (c *Client) GetUserProfile(ctx context.Context, userID int64) (UserProfile, error) {
endpoint := c.cfg.Java.OtherBaseURL + "/user-profile/client/getByUserId?userId=" + url.QueryEscape(strconv.FormatInt(userID, 10))
var resp resultResponse[UserProfile]
if err := c.getJSON(ctx, endpoint, nil, &resp); err != nil {
return UserProfile{}, err
}
if int64(resp.Body.ID) == 0 {
return UserProfile{}, fmt.Errorf("empty user profile response")
}
return resp.Body, nil
}
func (c *Client) GetUserRegion(ctx context.Context, userID int64, authorization string) (UserRegion, error) {
if userID <= 0 {
return UserRegion{}, fmt.Errorf("userId is required")
}
endpoint := c.cfg.Java.AppBaseURL + "/user/user-region?userId=" + url.QueryEscape(strconv.FormatInt(userID, 10))
headers := javaAppHeadersFromAuthorization(authorization)
if strings.TrimSpace(authorization) != "" {
headers.Set("Authorization", authorization)
}
var resp resultResponse[UserRegion]
if err := c.getJSON(ctx, endpoint, headers, &resp); err != nil {
return UserRegion{}, err
}
return resp.Body, nil
}
func (c *Client) ResolveRegionCodeByCountryCode(ctx context.Context, sysOrigin string, countryCode string) (string, error) {
countryCode = strings.ToUpper(strings.TrimSpace(countryCode))
if countryCode == "" {
return "", nil
}
configs, err := c.ListRegionConfigs(ctx, sysOrigin)
if err != nil {
return "", err
}
for _, item := range configs {
if item.Del {
continue
}
if !regionConfigContainsCountryCode(item.CountryCodes, countryCode) {
continue
}
regionCode := strings.TrimSpace(item.RegionCode)
if regionCode != "" {
return regionCode, nil
}
}
return "", nil
}
func (c *Client) ListRegionConfigs(ctx context.Context, sysOrigin string) ([]RegionConfig, error) {
sysOrigin = strings.ToUpper(strings.TrimSpace(sysOrigin))
if sysOrigin == "" {
sysOrigin = "LIKEI"
}
now := time.Now()
c.regionConfigCacheMu.Lock()
if entry, ok := c.regionConfigCacheMap[sysOrigin]; ok && now.Before(entry.expiresAt) {
items := append([]RegionConfig(nil), entry.items...)
c.regionConfigCacheMu.Unlock()
return items, nil
}
c.regionConfigCacheMu.Unlock()
endpoint := strings.TrimRight(c.cfg.Java.OtherBaseURL, "/") + "/region-config/client/listByCondition"
var resp resultResponse[[]RegionConfig]
if err := c.postJSON(ctx, endpoint, map[string]string{"sysOrigin": sysOrigin}, nil, &resp); err != nil {
return nil, err
}
items := append([]RegionConfig(nil), resp.Body...)
c.regionConfigCacheMu.Lock()
if c.regionConfigCacheMap == nil {
c.regionConfigCacheMap = map[string]regionConfigCacheEntry{}
}
c.regionConfigCacheMap[sysOrigin] = regionConfigCacheEntry{
expiresAt: now.Add(regionConfigCacheTTL),
items: append([]RegionConfig(nil), items...),
}
c.regionConfigCacheMu.Unlock()
return items, nil
}
func regionConfigContainsCountryCode(countryCodes string, countryCode string) bool {
countryCode = strings.ToUpper(strings.TrimSpace(countryCode))
if countryCode == "" {
return false
}
for _, item := range strings.Split(countryCodes, ",") {
if strings.ToUpper(strings.TrimSpace(item)) == countryCode {
return true
}
}
return false
}
func (c *Client) GetUserProfileByAccount(ctx context.Context, sysOrigin, account string) (UserProfile, error) {
endpoint := c.cfg.Java.OtherBaseURL + "/user-profile/client/getByAccount?sysOrigin=" +
url.QueryEscape(strings.TrimSpace(sysOrigin)) + "&account=" + url.QueryEscape(strings.TrimSpace(account))
var resp resultResponse[UserProfile]
if err := c.getJSON(ctx, endpoint, nil, &resp); err != nil {
return UserProfile{}, err
}
if int64(resp.Body.ID) == 0 {
return UserProfile{}, fmt.Errorf("empty user profile response")
}
return resp.Body, nil
}
func (c *Client) GetUserProfileByAccountOne(ctx context.Context, account string) (UserProfile, error) {
endpoint := c.cfg.Java.OtherBaseURL + "/user-profile/client/getByAccountOne?account=" +
url.QueryEscape(strings.TrimSpace(account))
var resp resultResponse[UserProfile]
if err := c.getJSON(ctx, endpoint, nil, &resp); err != nil {
return UserProfile{}, err
}
if int64(resp.Body.ID) == 0 {
return UserProfile{}, fmt.Errorf("empty user profile response")
}
return resp.Body, nil
}
func (c *Client) GetTeamPolicyRelease(ctx context.Context, query TeamPolicyReleaseQuery) (TeamPolicyRelease, error) {
values := url.Values{}
values.Set("sysOrigin", strings.TrimSpace(query.SysOrigin))
values.Set("region", strings.TrimSpace(query.Region))
values.Set("policyType", strings.TrimSpace(query.PolicyType))
values.Set("countryCode", strings.TrimSpace(query.CountryCode))
endpoint := c.cfg.Java.OtherBaseURL + "/team-policy/client/getReleaseByRegionAndTypeAndPolicyType?" + values.Encode()
var resp resultResponse[TeamPolicyRelease]
if err := c.getJSON(ctx, endpoint, nil, &resp); err != nil {
return TeamPolicyRelease{}, err
}
return resp.Body, nil
}
func (c *Client) ListCurrentWeekRoomContribution(ctx context.Context, limit int) ([]RoomContributionActivityCount, error) {
if limit <= 0 {
limit = 200
}
endpoint := c.cfg.Java.OtherBaseURL + "/room-contribution-activity-count/client/listCurrentWeek?limit=" +
url.QueryEscape(strconv.Itoa(limit))
var resp resultResponse[[]RoomContributionActivityCount]
if err := c.getJSON(ctx, endpoint, nil, &resp); err != nil {
return nil, err
}
return resp.Body, nil
}
func (c *Client) GetCurrentWeekRoomContribution(ctx context.Context, roomID int64) (RoomContributionActivityCount, error) {
if roomID <= 0 {
return RoomContributionActivityCount{}, nil
}
endpoint := c.cfg.Java.OtherBaseURL + "/room-contribution-activity-count/client/currentWeek?roomId=" +
url.QueryEscape(strconv.FormatInt(roomID, 10))
var resp resultResponse[RoomContributionActivityCount]
if err := c.getJSON(ctx, endpoint, nil, &resp); err != nil {
return RoomContributionActivityCount{}, err
}
return resp.Body, nil
}
func (c *Client) MarkRoomTurnoverRewardCoinsSent(ctx context.Context, id string) (bool, error) {
id = strings.TrimSpace(id)
if id == "" {
return false, fmt.Errorf("empty room contribution id")
}
endpoint := c.cfg.Java.OtherBaseURL + "/room-contribution-activity-count/client/rewardCoinsSent?id=" + url.QueryEscape(id)
var resp resultResponse[bool]
if err := c.postJSON(ctx, endpoint, nil, nil, &resp); err != nil {
return false, err
}
return resp.Body, nil
}
func (c *Client) GetRoomProfileByUserID(ctx context.Context, userID int64) (RoomProfile, error) {
if userID <= 0 {
return RoomProfile{}, nil
}
endpoint := c.cfg.Java.OtherBaseURL + "/room-manager/client/getProfileByUserId?userId=" +
url.QueryEscape(strconv.FormatInt(userID, 10))
var resp resultResponse[RoomProfile]
if err := c.getJSON(ctx, endpoint, nil, &resp); err != nil {
return RoomProfile{}, err
}
return resp.Body, nil
}
func (c *Client) MapRoomProfiles(ctx context.Context, roomIDs []int64) (map[int64]RoomProfile, error) {
if len(roomIDs) == 0 {
return map[int64]RoomProfile{}, nil
}
endpoint := c.cfg.Java.OtherBaseURL + "/room-manager/client/mapProfileByRoomIds"
var resp resultResponse[map[string]RoomProfile]
if err := c.postJSON(ctx, endpoint, roomIDs, nil, &resp); err != nil {
return nil, err
}
result := make(map[int64]RoomProfile, len(resp.Body))
for key, value := range resp.Body {
roomID, err := strconv.ParseInt(strings.TrimSpace(key), 10, 64)
if err != nil {
roomID = int64(value.ID)
}
if roomID <= 0 {
continue
}
result[roomID] = value
}
return result, nil
}
func (c *Client) rewardBridgeBaseURL() string {
if baseURL := strings.TrimRight(strings.TrimSpace(c.cfg.Java.ConsoleBaseURL), "/"); baseURL != "" {
return baseURL
}
return strings.TrimRight(strings.TrimSpace(c.cfg.Java.BridgeBaseURL), "/")
}
func (c *Client) GetUserLanguage(ctx context.Context, userID int64) (string, error) {
endpoint := c.cfg.Java.OtherBaseURL + "/user-profile/client/getLanguage?userId=" + url.QueryEscape(strconv.FormatInt(userID, 10))
var resp resultResponse[string]
if err := c.getJSON(ctx, endpoint, nil, &resp); err != nil {
return "", err
}
return strings.TrimSpace(resp.Body), nil
}
func (c *Client) RemoveUserProfileCacheAll(ctx context.Context, userID int64) error {
endpoint := c.cfg.Java.OtherBaseURL + "/user-profile/client/removeCacheAll?userId=" + url.QueryEscape(strconv.FormatInt(userID, 10))
return c.getJSON(ctx, endpoint, nil, nil)
}
func (c *Client) PunishAccount(ctx context.Context, req PunishAccountRequest) error {
return c.postJSON(ctx, c.cfg.Java.OtherBaseURL+"/app-user/account/punishment", req, nil, nil)
}
func (c *Client) UnblockAccount(ctx context.Context, req UnblockAccountRequest) error {
return c.postJSON(ctx, c.cfg.Java.OtherBaseURL+"/app-user/account/unblock", req, nil, nil)
}
func (c *Client) MapGoldBalance(ctx context.Context, userIDs []int64) (map[int64]int64, error) {
endpoint := c.cfg.Java.WalletBaseURL + "/wallet/gold/client/mapBalance"
var resp resultResponse[map[string]int64]
if err := c.postJSON(ctx, endpoint, userIDs, nil, &resp); err != nil {
return nil, err
}
result := make(map[int64]int64, len(resp.Body))
for key, value := range resp.Body {
userID, err := strconv.ParseInt(key, 10, 64)
if err != nil {
continue
}
result[userID] = value
}
return result, nil
}
func (c *Client) ExistsGoldEvent(ctx context.Context, eventID string) (bool, error) {
endpoint := c.cfg.Java.WalletBaseURL + "/wallet/gold/client/existsEventId?eventId=" + url.QueryEscape(eventID)
var resp resultResponse[bool]
if err := c.getJSON(ctx, endpoint, nil, &resp); err != nil {
return false, err
}
return resp.Body, nil
}
func (c *Client) ChangeGoldBalance(ctx context.Context, cmd GoldReceiptCommand) error {
endpoint := c.cfg.Java.WalletBaseURL + "/wallet/gold/client/balance/change"
if err := c.postJSON(ctx, endpoint, cmd, nil, nil); err == nil {
return nil
} else if !shouldFallbackToGoldTestEndpoint(err) {
return err
}
// Some local environments only expose the lighter test endpoint payload.
testCmd := TestGoldReceiptCommand{
ReceiptType: cmd.ReceiptType,
UserID: cmd.UserID,
EventType: firstNonEmpty(cmd.CustomizeOrigin, cmd.Origin),
EventDesc: cmd.CustomizeOriginDesc,
EventID: cmd.EventID,
SysOrigin: cmd.SysOrigin,
Remark: cmd.Remark,
Amount: cmd.Amount.DollarAmount,
CloseDelayAsset: cmd.CloseDelayAsset,
}
return c.postJSON(ctx, c.cfg.Java.WalletBaseURL+"/wallet/gold/client/balance/change/test", testCmd, nil, nil)
}
func firstNonEmpty(values ...string) string {
for _, value := range values {
if strings.TrimSpace(value) != "" {
return value
}
}
return ""
}
func NewPennyAmountPayloadFromDollar(amount int64) PennyAmountPayload {
return PennyAmountPayload{
PennyAmount: amount * 100,
DollarAmount: amount,
}
}
func shouldFallbackToGoldTestEndpoint(err error) bool {
if err == nil {
return false
}
message := strings.ToLower(strings.TrimSpace(err.Error()))
switch {
case strings.Contains(message, "status=404"),
strings.Contains(message, "status=405"),
strings.Contains(message, "status=501"),
strings.Contains(message, "connection refused"),
strings.Contains(message, "no such host"),
strings.Contains(message, "unsupported protocol"):
return true
default:
return false
}
}
func (c *Client) getJSON(ctx context.Context, endpoint string, headers http.Header, out any) error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return err
}
applyHeaders(req.Header, headers)
resp, err := c.httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
return decodeResponse(resp, out)
}
func (c *Client) postJSON(ctx context.Context, endpoint string, payload any, headers http.Header, out any) error {
data, err := json.Marshal(payload)
if err != nil {
return err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(data))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
applyHeaders(req.Header, headers)
resp, err := c.httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
return decodeResponse(resp, out)
}
func decodeResponse(resp *http.Response, out any) error {
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
if resp.StatusCode >= http.StatusBadRequest {
return fmt.Errorf("java api failed: status=%d body=%s", resp.StatusCode, strings.TrimSpace(string(body)))
}
if out == nil || len(body) == 0 {
return nil
}
if err := json.Unmarshal(body, out); err != nil {
return fmt.Errorf("decode response failed: %w body=%s", err, strings.TrimSpace(string(body)))
}
return nil
}
func applyHeaders(dst http.Header, src http.Header) {
for key, values := range src {
for _, value := range values {
dst.Add(key, value)
}
}
}