package integration import ( "bytes" "chatapp-cron/internal/config" "context" "encoding/json" "fmt" "io" "net/http" "net/url" "strconv" "strings" ) type Client struct { cfg config.Config httpClient *http.Client } type SendActivityRewardRequest struct { TrackID int64 `json:"trackId"` Origin string `json:"origin"` SysOrigin string `json:"sysOrigin"` SourceGroupID int64 `json:"sourceGroupId"` AcceptUserID int64 `json:"acceptUserId"` } type UserProfile struct { ID Int64Value `json:"id"` UserAvatar string `json:"userAvatar"` UserNickname string `json:"userNickname"` } 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"` Origin string `json:"origin,omitempty"` Remark string `json:"remark,omitempty"` Amount PennyAmountPayload `json:"amount"` CloseDelayAsset bool `json:"closeDelayAsset"` OpUserType string `json:"opUserType"` CustomizeOrigin string `json:"customizeOrigin,omitempty"` CustomizeOriginDesc string `json:"customizeOriginDesc,omitempty"` } type PennyAmountPayload struct { PennyAmount int64 `json:"pennyAmount,string"` DollarAmount int64 `json:"dollarAmount"` } 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 } func New(cfg config.Config) *Client { return &Client{ cfg: cfg, httpClient: &http.Client{ Timeout: cfg.Timeout, }, } } func (c *Client) SendActivityReward(ctx context.Context, req SendActivityRewardRequest) error { var resp resultResponse[json.RawMessage] if err := c.postJSON(ctx, c.cfg.JavaOtherBaseURL+"/props-activity-cnf/client/sendActivityReward", req, &resp); err != nil { return err } return ensureSuccessfulResult(resp.Code, resp.Message, resp.Success) } func (c *Client) ListLastWeekRoomContribution(ctx context.Context) ([]RoomContributionActivityCount, error) { endpoint := c.cfg.JavaOtherBaseURL + "/room-contribution-activity-count/client/listLastWeek" var resp resultResponse[[]RoomContributionActivityCount] if err := c.getJSON(ctx, endpoint, &resp); err != nil { return nil, 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.JavaOtherBaseURL + "/room-contribution-activity-count/client/rewardCoinsSent?id=" + url.QueryEscape(id) var resp resultResponse[bool] if err := c.postEmpty(ctx, endpoint, &resp); err != nil { return false, 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.JavaOtherBaseURL + "/room-manager/client/mapProfileByRoomIds" var resp resultResponse[map[string]RoomProfile] if err := c.postJSON(ctx, endpoint, roomIDs, &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) ChangeGoldBalance(ctx context.Context, cmd GoldReceiptCommand) error { var resp resultResponse[json.RawMessage] if err := c.postJSON(ctx, c.cfg.JavaWalletBaseURL+"/wallet/gold/client/balance/change", cmd, &resp); err != nil { return err } return ensureSuccessfulResult(resp.Code, resp.Message, resp.Success) } func NewPennyAmountPayloadFromDollar(amount int64) PennyAmountPayload { return PennyAmountPayload{ PennyAmount: amount * 100, DollarAmount: amount, } } 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, &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) getJSON(ctx context.Context, endpoint string, out any) error { req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) if err != nil { return err } 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, 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") resp, err := c.httpClient.Do(req) if err != nil { return err } defer resp.Body.Close() return decodeResponse(resp, out) } func (c *Client) postEmpty(ctx context.Context, endpoint string, out any) error { req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, nil) if err != nil { return err } 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 ensureSuccessfulResult(code int, message string, success *bool) error { if success != nil && !*success { return fmt.Errorf("java api returned unsuccessful result: code=%d message=%s", code, strings.TrimSpace(message)) } if code != 0 && code != 200 { return fmt.Errorf("java api returned error result: code=%d message=%s", code, strings.TrimSpace(message)) } return nil }