Files
007pay-sdk/client.go
T
2026-07-10 17:18:58 +08:00

244 lines
5.6 KiB
Go

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)
}