305 lines
9.0 KiB
Go

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)
}
}
}