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"` Name string `json:"name"` Content string `json:"content"` Quantity Int64Value `json:"quantity"` Cover string `json:"cover"` 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) 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) 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) } } }