package integration import ( "bytes" "chatapp3-golang/internal/config" "context" "encoding/json" "fmt" "io" "net/http" "net/url" "strconv" "strings" ) type Client struct { cfg config.Config httpClient *http.Client } type UserCredential struct { UserID Int64Value `json:"userId"` SysOrigin string `json:"sysOrigin"` } type UserProfile struct { ID Int64Value `json:"id"` UserAvatar string `json:"userAvatar"` UserNickname string `json:"userNickname"` } 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 int64 `json:"amount"` CloseDelayAsset bool `json:"closeDelayAsset"` OpUserType string `json:"opUserType"` CustomizeOrigin string `json:"customizeOrigin,omitempty"` CustomizeOriginDesc string `json:"customizeOriginDesc,omitempty"` } 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 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 } func New(cfg config.Config) *Client { return &Client{ cfg: cfg, httpClient: &http.Client{ Timeout: cfg.Timeout, }, } } func (c *Client) AuthenticateToken(ctx context.Context, token string) (UserCredential, error) { endpoint := c.cfg.JavaAuthBaseURL + "/auth/client/getUserCredentialByToken?token=" + url.QueryEscape(token) 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") } return resp.Body, nil } func (c *Client) GetDeviceFingerprint(ctx context.Context, userID int64) (string, error) { endpoint := c.cfg.JavaDeviceBaseURL + "/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.JavaAppBaseURL + "/activity/invite/user/my/invite/code" headers := http.Header{} if authorization != "" { headers.Set("Authorization", authorization) } var resp InviteCode if err := c.getJSON(ctx, endpoint, headers, &resp); err != nil { return InviteCode{}, err } if strings.TrimSpace(resp.InviteCode) == "" { return InviteCode{}, fmt.Errorf("invite code response is empty") } return resp, nil } func (c *Client) GrantGold(ctx context.Context, req GrantGoldRequest) error { return c.postJSON(ctx, c.cfg.JavaBridgeBaseURL+"/internal/resident-activity/invite/grant-gold", req, nil, nil) } func (c *Client) GrantProps(ctx context.Context, req GrantPropsRequest) error { return c.postJSON(ctx, c.cfg.JavaBridgeBaseURL+"/internal/resident-activity/invite/grant-props", req, nil, nil) } func (c *Client) GetUserProfile(ctx context.Context, userID int64) (UserProfile, error) { endpoint := c.cfg.JavaOtherBaseURL + "/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) GetUserLanguage(ctx context.Context, userID int64) (string, error) { endpoint := c.cfg.JavaOtherBaseURL + "/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) MapGoldBalance(ctx context.Context, userIDs []int64) (map[int64]int64, error) { endpoint := c.cfg.JavaWalletBaseURL + "/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.JavaWalletBaseURL + "/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.JavaWalletBaseURL + "/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: cmd.CustomizeOrigin, EventDesc: cmd.CustomizeOriginDesc, EventID: cmd.EventID, SysOrigin: cmd.SysOrigin, Remark: cmd.Remark, Amount: cmd.Amount, CloseDelayAsset: cmd.CloseDelayAsset, } return c.postJSON(ctx, c.cfg.JavaWalletBaseURL+"/wallet/gold/client/balance/change/test", testCmd, nil, nil) } 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) } } }