package pay007 import ( "bytes" "context" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "strconv" "strings" "time" ) const defaultUserAgent = "007pay-go-sdk/1.0" type Client struct { baseURL string pid string secret string httpClient *http.Client userAgent string } type Option func(*Client) func WithHTTPClient(httpClient *http.Client) Option { return func(c *Client) { if httpClient != nil { c.httpClient = httpClient } } } func WithTimeout(timeout time.Duration) Option { return func(c *Client) { if timeout > 0 { c.httpClient.Timeout = timeout } } } func WithUserAgent(userAgent string) Option { return func(c *Client) { if strings.TrimSpace(userAgent) != "" { c.userAgent = userAgent } } } func NewClient(baseURL, pid, secret string, opts ...Option) (*Client, error) { baseURL = strings.TrimRight(strings.TrimSpace(baseURL), "/") if baseURL == "" { return nil, errors.New("baseURL is required") } if _, err := url.ParseRequestURI(baseURL); err != nil { return nil, fmt.Errorf("invalid baseURL: %w", err) } if strings.TrimSpace(pid) == "" { return nil, errors.New("pid is required") } if strings.TrimSpace(secret) == "" { return nil, errors.New("secret is required") } c := &Client{ baseURL: baseURL, pid: pid, secret: secret, httpClient: &http.Client{ Timeout: 15 * time.Second, }, userAgent: defaultUserAgent, } for _, opt := range opts { opt(c) } return c, nil } func (c *Client) Sign(params map[string]string) string { return BuildSign(params, c.secret) } func (c *Client) Verify(params map[string]string) bool { return VerifySign(params, c.secret) } func (c *Client) Submit(ctx context.Context, req SubmitRequest) (*SubmitResponse, error) { var out SubmitResponse if err := c.doSigned(ctx, http.MethodPost, "/api/pay/submit", req.params(), &out); err != nil { return nil, err } return &out, nil } func (c *Client) Query(ctx context.Context, req QueryRequest) (*QueryResponse, error) { var out QueryResponse if err := c.doSigned(ctx, http.MethodGet, "/api/pay/query", req.params(), &out); err != nil { return nil, err } return &out, nil } func (c *Client) Refund(ctx context.Context, req RefundRequest) (*RefundResponse, error) { var out RefundResponse if err := c.doSigned(ctx, http.MethodPost, "/api/pay/refund", req.params(), &out); err != nil { return nil, err } return &out, nil } func (c *Client) Orders(ctx context.Context, req OrdersRequest) (*OrdersResponse, error) { var out OrdersResponse if err := c.doSigned(ctx, http.MethodGet, "/api/pay/orders", req.params(), &out); err != nil { return nil, err } return &out, nil } func (c *Client) Manual(ctx context.Context, req ManualRequest) (*ManualResponse, error) { var out ManualResponse if err := c.doSigned(ctx, http.MethodPost, "/api/pay/manual", req.params(), &out); err != nil { return nil, err } return &out, nil } func (c *Client) Stats(ctx context.Context, req StatsRequest) (*StatsResponse, error) { var out StatsResponse if err := c.doSigned(ctx, http.MethodGet, "/api/pay/stats", req.params(), &out); err != nil { return nil, err } return &out, nil } func (c *Client) doSigned(ctx context.Context, method, path string, params map[string]string, out any) error { signed := compactParams(params) signed["pid"] = c.pid signed["sign_type"] = "MD5" signed["sign"] = BuildSign(signed, c.secret) values := url.Values{} for k, v := range signed { values.Set(k, v) } endpoint := c.baseURL + path var body io.Reader if method == http.MethodGet { endpoint += "?" + values.Encode() } else { body = bytes.NewBufferString(values.Encode()) } httpReq, err := http.NewRequestWithContext(ctx, method, endpoint, body) if err != nil { return err } httpReq.Header.Set("Accept", "application/json") httpReq.Header.Set("User-Agent", c.userAgent) if method != http.MethodGet { httpReq.Header.Set("Content-Type", "application/x-www-form-urlencoded") } resp, err := c.httpClient.Do(httpReq) if err != nil { return err } defer resp.Body.Close() respBody, err := io.ReadAll(resp.Body) if err != nil { return err } var envelope struct { Code int `json:"code"` Message string `json:"message"` Data json.RawMessage `json:"data"` } if err := json.Unmarshal(respBody, &envelope); err != nil { return &APIError{ HTTPStatus: resp.StatusCode, Code: 0, Message: "invalid json response: " + err.Error(), Body: string(respBody), } } if resp.StatusCode < 200 || resp.StatusCode >= 300 || envelope.Code != 200 { return &APIError{ HTTPStatus: resp.StatusCode, Code: envelope.Code, Message: envelope.Message, Body: string(respBody), } } if out == nil || len(envelope.Data) == 0 || string(envelope.Data) == "null" { return nil } if err := json.Unmarshal(envelope.Data, out); err != nil { return &APIError{ HTTPStatus: resp.StatusCode, Code: envelope.Code, Message: "invalid data response: " + err.Error(), Body: string(respBody), } } return nil } type APIError struct { HTTPStatus int Code int Message string Body string } func (e *APIError) Error() string { if e == nil { return "" } return fmt.Sprintf("007pay api error: http=%d code=%d message=%s", e.HTTPStatus, e.Code, e.Message) } func compactParams(params map[string]string) map[string]string { out := make(map[string]string, len(params)) for k, v := range params { if strings.TrimSpace(k) == "" || v == "" { continue } out[k] = v } return out } func itoa(v int) string { return strconv.Itoa(v) }