diff --git a/internal/provider/rainyun/billing.go b/internal/provider/rainyun/billing.go index 5a78ebc..6a48487 100644 --- a/internal/provider/rainyun/billing.go +++ b/internal/provider/rainyun/billing.go @@ -21,7 +21,7 @@ func (b *BillingService) Orders(ctx context.Context) ([]provider.Order, error) { CreateDate string `json:"create_date"` } var raw []rainyunOrder - if err := b.client.getWithQuery(ctx, "/expense/orders/list", map[string]string{"options": "1"}, &raw); err != nil { + if err := b.client.getList(ctx, "/expense/orders/list", &raw); err != nil { return nil, err } orders := make([]provider.Order, len(raw)) diff --git a/internal/provider/rainyun/client.go b/internal/provider/rainyun/client.go index 0b1bd3e..a845685 100644 --- a/internal/provider/rainyun/client.go +++ b/internal/provider/rainyun/client.go @@ -44,6 +44,23 @@ func (c *client) getWithQuery(ctx context.Context, path string, query map[string return c.do(ctx, "GET", path, nil, result, query) } +// getList sends a GET request to a Rainyun list endpoint and extracts the "records" field. +func (c *client) getList(ctx context.Context, path string, result any) error { + var wrapper listResponse + if err := c.do(ctx, "GET", path, nil, &wrapper, listOptions()); err != nil { + return err + } + // Unmarshal the records array into the result + recordsJSON, err := json.Marshal(wrapper.Records) + if err != nil { + return fmt.Errorf("marshal records: %w", err) + } + if err := json.Unmarshal(recordsJSON, result); err != nil { + return fmt.Errorf("unmarshal records: %w", err) + } + return nil +} + func (c *client) post(ctx context.Context, path string, body, result any) error { return c.do(ctx, "POST", path, body, result, nil) } diff --git a/internal/provider/rainyun/domain.go b/internal/provider/rainyun/domain.go index 9d4dd72..603a7ae 100644 --- a/internal/provider/rainyun/domain.go +++ b/internal/provider/rainyun/domain.go @@ -20,7 +20,7 @@ type rainyunDomain struct { func (d *DomainService) List(ctx context.Context) ([]provider.Domain, error) { var raw []rainyunDomain - if err := d.client.getWithQuery(ctx, "/product/domain/", map[string]string{"options": "1"}, &raw); err != nil { + if err := d.client.getList(ctx, "/product/domain/", &raw); err != nil { return nil, err } domains := make([]provider.Domain, len(raw)) diff --git a/internal/provider/rainyun/listdata.go b/internal/provider/rainyun/listdata.go new file mode 100644 index 0000000..ee83eab --- /dev/null +++ b/internal/provider/rainyun/listdata.go @@ -0,0 +1,21 @@ +package rainyun + +// listOptions returns the standard pagination query for Rainyun list endpoints. +// Format verified via browser DevTools capture of the Rainyun web console: +// +// https://api.v2.rainyun.com/product/rcs/?options={url-encoded JSON} +// +// The decoded JSON is: {"columnFilters":{},"sort":[],"page":1,"perPage":20} +func listOptions() map[string]string { + return map[string]string{ + "options": `{"columnFilters":{},"sort":[],"page":1,"perPage":50}`, + } +} + +// listResponse is the JSON envelope returned by Rainyun list endpoints. +// The API wraps data in {"code":0,"data":{"records":[...],"totalRecords":N}}. +// Our client extracts apiResp.Data ({"records":[...]}), then we unmarshal into this. +type listResponse struct { + Records []any `json:"records"` + TotalRecords int `json:"totalRecords"` +} diff --git a/internal/provider/rainyun/server.go b/internal/provider/rainyun/server.go index aeebd05..d602884 100644 --- a/internal/provider/rainyun/server.go +++ b/internal/provider/rainyun/server.go @@ -29,7 +29,7 @@ type rainyunServer struct { func (s *ServerService) List(ctx context.Context) ([]provider.Server, error) { var raw []rainyunServer - if err := s.client.getWithQuery(ctx, "/product/rcs/", map[string]string{"options": "1"}, &raw); err != nil { + if err := s.client.getList(ctx, "/product/rcs/", &raw); err != nil { return nil, err } servers := make([]provider.Server, len(raw)) diff --git a/internal/provider/rainyun/server_test.go b/internal/provider/rainyun/server_test.go index 2171fbd..4a7c5c6 100644 --- a/internal/provider/rainyun/server_test.go +++ b/internal/provider/rainyun/server_test.go @@ -15,18 +15,21 @@ func TestServerList(t *testing.T) { } resp := map[string]any{ "code": 0, - "data": []map[string]any{ - { - "id": 12001, - "name": "web-server", - "status": "running", - "ip": "1.2.3.4", - "vcpu": 2, - "memory": 4096, - "disk": 50, - "os": "Ubuntu 22.04", - "expire_date": "2026-08-15", + "data": map[string]any{ + "records": []map[string]any{ + { + "id": 12001, + "name": "web-server", + "status": "running", + "ip": "1.2.3.4", + "vcpu": 2, + "memory": 4096, + "disk": 50, + "os": "Ubuntu 22.04", + "expire_date": "2026-08-15", + }, }, + "totalRecords": 1, }, } json.NewEncoder(w).Encode(resp) diff --git a/internal/provider/rainyun/storage.go b/internal/provider/rainyun/storage.go index 1d5c12e..f7ba289 100644 --- a/internal/provider/rainyun/storage.go +++ b/internal/provider/rainyun/storage.go @@ -17,7 +17,7 @@ func (s *StorageService) List(ctx context.Context) ([]provider.StorageInstance, Name string `json:"name"` Status string `json:"status"` } - if err := s.client.getWithQuery(ctx, "/product/ros/instance", map[string]string{"options": "1"}, &raw); err != nil { + if err := s.client.getList(ctx, "/product/ros/instance", &raw); err != nil { return nil, err } instances := make([]provider.StorageInstance, len(raw))