383 lines
12 KiB
Go

package integration
import (
"bytes"
"chatapp3-golang/internal/config"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
)
type Client struct {
cfg config.Config
httpClient *http.Client
}
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 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) {
tokenCredential, err := parseUserCredentialToken(token)
if err != nil {
return UserCredential{}, err
}
endpoint := c.cfg.JavaAuthBaseURL + "/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 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.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)
}
}
}