diff --git a/apis/common/query.go b/apis/common/query.go new file mode 100644 index 0000000..3e0ed0f --- /dev/null +++ b/apis/common/query.go @@ -0,0 +1,81 @@ +package common + +import ( + "fmt" + "reflect" + "strconv" + "strings" + + "github.com/bytedance/sonic" +) + +// MarshalQueryParams 基于 struct 字段的 json tag 自动构建 URL 查询参数 map。 +// +// 规则: +// - 字段必须有 json tag,tag 名(逗号前的部分)作为参数 key +// - 指针字段为 nil 时跳过(对应 optional 语义);非 nil 时解引用处理 +// - 基础类型(string / int / uint / float / bool)转为字符串 +// - 复杂类型(struct / map / slice 等)自动 marshal 为 JSON 字符串 +// - 无 json tag 或 tag 为 "-" 的字段被忽略 +func MarshalQueryParams(v any) (map[string]string, error) { + rv := reflect.ValueOf(v) + if rv.Kind() == reflect.Ptr { + if rv.IsNil() { + return nil, nil + } + rv = rv.Elem() + } + if rv.Kind() != reflect.Struct { + return nil, fmt.Errorf("MarshalQueryParams: 期望 struct,实际 %s", rv.Kind()) + } + + rst := make(map[string]string) + rt := rv.Type() + for i := 0; i < rt.NumField(); i++ { + field := rt.Field(i) + tag := field.Tag.Get("json") + if tag == "" || tag == "-" { + continue + } + name, _, _ := strings.Cut(tag, ",") + + fv := rv.Field(i) + if fv.Kind() == reflect.Ptr { + if fv.IsNil() { + continue + } + fv = fv.Elem() + } + + val, err := queryValue(fv) + if err != nil { + return nil, fmt.Errorf("MarshalQueryParams: 字段 %s: %w", field.Name, err) + } + rst[name] = val + } + return rst, nil +} + +func queryValue(v reflect.Value) (string, error) { + switch v.Kind() { + case reflect.String: + return v.String(), nil + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return strconv.FormatInt(v.Int(), 10), nil + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return strconv.FormatUint(v.Uint(), 10), nil + case reflect.Float32, reflect.Float64: + return strconv.FormatFloat(v.Float(), 'f', -1, 64), nil + case reflect.Bool: + return strconv.FormatBool(v.Bool()), nil + default: + if !v.CanInterface() { + return "", fmt.Errorf("无法读取未导出字段") + } + b, err := sonic.Marshal(v.Interface()) + if err != nil { + return "", err + } + return string(b), nil + } +} diff --git a/apis/common/query_test.go b/apis/common/query_test.go new file mode 100644 index 0000000..f7f37ce --- /dev/null +++ b/apis/common/query_test.go @@ -0,0 +1,72 @@ +package common + +import ( + "reflect" + "testing" +) + +type queryParamsSample struct { + IsRGpu *bool `json:"is_rgpu"` + Limit int `json:"limit"` + Name string `json:"name"` + Options StandQueryParameters `json:"options"` + Tags []string `json:"tags"` + Skip string `json:"-"` + NoTag string +} + +func TestMarshalQueryParams(t *testing.T) { + isGPU := true + s := &queryParamsSample{ + IsRGpu: &isGPU, + Limit: 10, + Name: "hello", + Options: StandQueryParameters{ + Page: 2, + PerPage: 20, + }, + Tags: []string{"a", "b"}, + } + + m, err := MarshalQueryParams(s) + if err != nil { + t.Fatalf("MarshalQueryParams() error = %v", err) + } + + want := map[string]string{ + "is_rgpu": "true", + "limit": "10", + "name": "hello", + "options": `{"columnFilters":{},"sort":null,"page":2,"perPage":20}`, + "tags": `["a","b"]`, + } + if !reflect.DeepEqual(m, want) { + t.Errorf("MarshalQueryParams() = %v, want %v", m, want) + } +} + +func TestMarshalQueryParamsNilPtr(t *testing.T) { + s := &queryParamsSample{} // IsRGpu 为 nil + m, err := MarshalQueryParams(s) + if err != nil { + t.Fatalf("MarshalQueryParams() error = %v", err) + } + + // nil 指针跳过,无 json tag 和 "-" 的字段跳过 + if _, ok := m["is_rgpu"]; ok { + t.Errorf("nil 指针字段 is_rgpu 不应输出") + } + if _, ok := m["Skip"]; ok { + t.Errorf("tag 为 '-' 的字段不应输出") + } + if _, ok := m["NoTag"]; ok { + t.Errorf("无 json tag 的字段不应输出") + } +} + +func TestMarshalQueryParamsNonStruct(t *testing.T) { + _, err := MarshalQueryParams(42) + if err == nil { + t.Errorf("非 struct 入参应返回错误") + } +} diff --git a/apis/public/get_status.go b/apis/public/get_status.go index f4c0b60..6eea6fc 100644 --- a/apis/public/get_status.go +++ b/apis/public/get_status.go @@ -36,6 +36,7 @@ type NodeStatusRecordItem struct { } func (req *GetStatusRequest) BuildQueryMap() (map[string]string, error) { + // TODO: migrate to unified method rst := make(map[string]string, 1) optionString, err := sonic.Marshal(req.Options) if err != nil { diff --git a/apis/rcs/client.go b/apis/rcs/client.go new file mode 100644 index 0000000..523f6e9 --- /dev/null +++ b/apis/rcs/client.go @@ -0,0 +1,11 @@ +package rcs + +import "github.com/XingfenD/rainyun_api_go_sdk/apis" + +type RcsService struct { + client *apis.RyClient +} + +func NewRcsService(c *apis.RyClient) *RcsService { + return &RcsService{client: c} +} diff --git a/apis/rcs/get_rcs_list.go b/apis/rcs/get_rcs_list.go new file mode 100644 index 0000000..331e9ff --- /dev/null +++ b/apis/rcs/get_rcs_list.go @@ -0,0 +1,208 @@ +package rcs + +import ( + "github.com/XingfenD/rainyun_api_go_sdk/apis/common" + "github.com/XingfenD/rainyun_api_go_sdk/constant" +) + +type GetRcsListResponse struct { + Code int `json:"code"` + Data RcsListResponse `json:"data"` +} + +type Node struct { + UUID string `json:"UUID"` + AuthKey string `json:"AuthKey"` + Region string `json:"Region"` + IPRegion string `json:"IpRegion"` + Machine string `json:"Machine"` + Product string `json:"Product"` + Subtype string `json:"Subtype"` + ChineseName string `json:"ChineseName"` + PhysicalNode string `json:"PhysicalNode"` + Config string `json:"Config"` + Stock any `json:"Stock"` // NOTE: any? + StatusData string `json:"StatusData"` + ShowMonitorData string `json:"ShowMonitorData"` + UpdateTime string `json:"UpdateTime"` + GitRepositoryName string `json:"GitRepositoryName"` + CertifyRequired bool `json:"CertifyRequired"` + IsDisableBackup bool `json:"IsDisableBackup"` + IsHidden bool `json:"IsHidden"` + NodeName string `json:"NodeName"` +} + +// NOTE: strange struct naming, needing checking +type DiskUsageData struct { + NAMING_FAILED struct { + Total int64 `json:"Total"` + Used int64 `json:"Used"` + } `json:"/"` // NOTE: strange json tag +} + +type UsageData struct { + CPU float64 `json:"CPU"` + MaxMem int64 `json:"MaxMem"` + FreeMem int64 `json:"FreeMem"` + UsedMem int `json:"UsedMem"` + Disks DiskUsageData `json:"Disks"` + DiskRead int `json:"DiskRead"` + DiskWrite int `json:"DiskWrite"` + NetOut float64 `json:"NetOut"` + NetIn int `json:"NetIn"` + SmartHealth any `json:"SmartHealth"` + SmartTemp int `json:"SmartTemp"` + UpdateTime int `json:"UpdateTime"` +} + +type TrafficPrice struct { + Num300 int `json:"300"` + Num1024 int `json:"1024"` + Num2048 int `json:"2048"` +} + +type DiskPrice struct { + Ssd float64 `json:"ssd"` + Hdd float64 `json:"hdd"` +} + +type Plan struct { + ID int `json:"id"` + Region string `json:"region"` + Subtype string `json:"subtype"` + PlanName string `json:"plan_name"` + Machine string `json:"machine"` + ChargeType string `json:"charge_type"` + Chinese string `json:"chinese"` + IsFree bool `json:"is_free"` + PointRenewPrice any `json:"point_renew_price"` + IsSelling bool `json:"is_selling"` + Price int `json:"price"` + TrafficBaseGb int `json:"traffic_base_gb"` + TrafficPrice TrafficPrice `json:"traffic_price"` + CPU int `json:"cpu"` + Memory int `json:"memory"` + NetIn int `json:"net_in"` + NetOut int `json:"net_out"` + IPPrices any `json:"ip_prices"` + IPSelling any `json:"ip_selling"` + AutoRestock int `json:"auto_restock"` + AvailableStock int `json:"available_stock"` + GpuMemorySize int `json:"gpu_memory_size"` + DgpuDevType string `json:"dgpu_dev_type"` + WebbarConfig any `json:"webbar_config"` + NoBackup bool `json:"no_backup"` + DiskPrice DiskPrice `json:"disk_price"` +} + +type OsInfo struct { + ID int `json:"id"` + Region string `json:"region"` + Subtype string `json:"subtype"` + Machine string `json:"machine"` + Name string `json:"name"` + Version string `json:"version"` + SyncStatus string `json:"sync_status"` + OsType string `json:"os_type"` + ChineseName string `json:"chinese_name"` + Icon string `json:"icon"` + IsWithBbr bool `json:"is_with_bbr"` + IsEol bool `json:"is_eol"` + IsAvailable bool `json:"is_available"` + Order int `json:"order"` + LatestFilename string `json:"latest_filename"` + NoVMAgent bool `json:"no_vm_agent"` +} + +type RcsRecord struct { + ExpDate int `json:"ExpDate"` + ExpireNotice int `json:"ExpireNotice"` + AutoRenew bool `json:"AutoRenew"` + UnsubscribeAble bool `json:"UnsubscribeAble"` + Try bool `json:"Try"` + ID int `json:"ID"` + UID int `json:"UID"` + PlanID int `json:"PlanID"` + CreateDate int `json:"CreateDate"` + NodeUUID string `json:"NodeUUID"` + Node Node `json:"Node"` + Status string `json:"Status"` + StopReason string `json:"StopReason"` + RewardPointsToBeCollect int `json:"RewardPointsToBeCollect"` + Tag string `json:"Tag"` + OsID int `json:"OsID"` + OsName string `json:"OsName"` + HostName string `json:"HostName"` + DefaultPass string `json:"DefaultPass"` + MainIPv4 string `json:"MainIPv4"` + IntIPv4 string `json:"IntIPv4"` + UsageData UsageData `json:"UsageData"` + Zone string `json:"Zone"` + NatPublicIP string `json:"NatPublicIP"` + NatPublicDomain string `json:"NatPublicDomain"` + NATSpareDomain string `json:"NATSpareDomain"` + NetIn int `json:"NetIn"` + NetOut int `json:"NetOut"` + NowNetIn int `json:"NowNetIn"` + NowNetOut int `json:"NowNetOut"` + NetMode string `json:"NetMode"` + BridgeSyncing bool `json:"BridgeSyncing"` + VnetID int `json:"VnetID"` + UpdateTime int `json:"UpdateTime"` + FwSyncTime int `json:"FwSyncTime"` + FwMode string `json:"FwMode"` + AbCPULimit int `json:"AbCpuLimit"` + AbNetLimit int `json:"AbNetLimit"` + AbWhiteReason string `json:"AbWhiteReason"` + TrafficBytes int64 `json:"TrafficBytes"` + TrafficResetDate int `json:"TrafficResetDate"` + TrafficBytesToday int `json:"TrafficBytesToday"` + TrafficBytesDayLimit int64 `json:"TrafficBytesDayLimit"` + TrafficOnLimit int `json:"TrafficOnLimit"` + Plan Plan `json:"Plan"` + OsInfo OsInfo `json:"OsInfo"` + CPU int `json:"CPU"` + Memory int `json:"Memory"` + Disk int `json:"Disk"` + RBSKeepLast int `json:"RBSKeepLast"` + RBSAutoBackup string `json:"RBSAutoBackup"` + RBSLastAutoBackupDate int `json:"RBSLastAutoBackupDate"` + FastAppInstallTaskKey string `json:"FastAppInstallTaskKey"` + GPUDevice string `json:"GPUDevice"` + GPUMemorySize int `json:"GPUMemorySize"` + DGPUEnable bool `json:"DGPUEnable"` + NoPrimaryGPU bool `json:"NoPrimaryGPU"` + WebbarMinutes int `json:"WebbarMinutes"` + WebbarResetDate int `json:"WebbarResetDate"` +} + +type RcsListResponse struct { + TotalRecords int `json:"TotalRecords"` + Records []RcsRecord `json:"Records"` +} + +type GetRcsListRequest struct { + IsRGpu *bool `json:"is_rgpu"` // optional + Options common.StandQueryParameters `json:"options"` +} + +func (req *GetRcsListRequest) BuildQueryMap() map[string]string { + m, err := common.MarshalQueryParams(req) + if err != nil { + return nil + } + return m +} + +func (s *RcsService) GetRcsList(req *GetRcsListRequest) (*GetRcsListResponse, error) { + path := "/product/rcs" + + var resp GetRcsListResponse + + querys, err := common.MarshalQueryParams(req) + if err != nil { + return nil, err + } + err = s.client.Do(constant.HTTPMethod_GET, path, querys, nil, &resp) + return &resp, err +} diff --git a/sdk/sdk.go b/sdk/sdk.go index 7b3d405..b499c04 100644 --- a/sdk/sdk.go +++ b/sdk/sdk.go @@ -3,15 +3,18 @@ package sdk import ( "github.com/XingfenD/rainyun_api_go_sdk/apis" "github.com/XingfenD/rainyun_api_go_sdk/apis/public" + "github.com/XingfenD/rainyun_api_go_sdk/apis/rcs" ) type RainyunSDK struct { public.PublicService + rcs.RcsService } func New(apiKey string) *RainyunSDK { c := apis.NewRyClient(apiKey) return &RainyunSDK{ PublicService: *public.NewPublicService(c), + RcsService: *rcs.NewRcsService(c), } }