commit c9d007497b80240beaa606334bd6970e4f23bbb7 Author: shiran Date: Fri Jul 10 17:18:58 2026 +0800 Initial 007Pay Go SDK diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2d751c5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/bin/ +/dist/ +*.exe +*.test +coverage.out +.env diff --git a/README.md b/README.md new file mode 100644 index 0000000..2dea157 --- /dev/null +++ b/README.md @@ -0,0 +1,147 @@ +# 007Pay Go Merchant SDK / CLI + +这是 007Pay 商户应用端 Go SDK,包含: + +- `pay007` SDK 包:创建订单、查询订单、退款、订单列表、手动入账、收益统计 +- `007pay` CLI:可在其他项目或脚本中快速调试接入 +- 与平台一致的 `pid + key + MD5` 签名工具 + +## 安装 + +作为独立子项目使用: + +```bash +cd merchant-sdk-go +go test ./... +go build -o bin/007pay ./cmd/007pay +``` + +发布到自己的 Git 仓库后,其他项目可直接引入: + +```bash +go get gitea.s1f.ren/shiran/007pay-sdk.git +``` + +## SDK 用法 + +```go +package main + +import ( + "context" + "fmt" + "log" + + pay007 "gitea.s1f.ren/shiran/007pay-sdk.git" +) + +func main() { + client, err := pay007.NewClient( + "http://127.0.0.1:8081", + "your-pid", + "your-secret-key", + ) + if err != nil { + log.Fatal(err) + } + + order, err := client.Submit(context.Background(), pay007.SubmitRequest{ + OutTradeNo: "M202607100001", + Name: "测试订单", + Money: "0.01", + Type: "alipay", + ClientType: "pc", + NotifyURL: "https://merchant.example.com/pay/notify", + ReturnURL: "https://merchant.example.com/pay/return", + }) + if err != nil { + log.Fatal(err) + } + + fmt.Println(order.CashierURL) +} +``` + +查询订单会调用平台商户查询接口;平台侧会在待支付时主动通过通道查询订单状态,例如支付宝会走 `alipay.trade.query`,并返回 `completed` 与 `channel_checked`。 + +```go +result, err := client.Query(context.Background(), pay007.QueryRequest{ + OutTradeNo: "M202607100001", +}) +if err != nil { + log.Fatal(err) +} +if result.Completed { + fmt.Println("paid") +} +``` + +## CLI 用法 + +CLI 支持环境变量: + +```bash +export PAY007_BASE_URL=http://127.0.0.1:8081 +export PAY007_PID=your-pid +export PAY007_KEY=your-secret-key +``` + +创建订单: + +```bash +007pay submit \ + --out-trade-no M202607100001 \ + --name "测试订单" \ + --money 0.01 \ + --type alipay \ + --clienttype pc \ + --notify-url https://merchant.example.com/pay/notify \ + --return-url https://merchant.example.com/pay/return +``` + +查询订单: + +```bash +007pay query --out-trade-no M202607100001 +``` + +退款: + +```bash +007pay refund --out-trade-no M202607100001 --money 0.01 --reason "用户退款" +``` + +订单列表: + +```bash +007pay orders --status 1 --page 1 --page-size 20 +``` + +收益统计: + +```bash +007pay stats --begin "2026-07-01 00:00:00" --end "2026-07-31 23:59:59" +``` + +签名调试: + +```bash +007pay sign out_trade_no=M202607100001 name=测试订单 money=0.01 type=alipay +``` + +## 签名规则 + +与平台 `utils/sign` 保持一致: + +1. 过滤空值、`sign`、`sign_type` +2. 参数按 key 升序排序 +3. 拼接为 `a=1&b=2` +4. 末尾追加应用密钥 +5. MD5 后转大写 + +SDK 中可直接使用: + +```go +sign := pay007.BuildSign(params, secret) +ok := pay007.VerifySign(params, secret) +``` diff --git a/client.go b/client.go new file mode 100644 index 0000000..97a332d --- /dev/null +++ b/client.go @@ -0,0 +1,243 @@ +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) +} diff --git a/client_test.go b/client_test.go new file mode 100644 index 0000000..75ac5ad --- /dev/null +++ b/client_test.go @@ -0,0 +1,69 @@ +package pay007 + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" +) + +func TestSubmitPostsSignedForm(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/pay/submit" { + t.Fatalf("path = %s", r.URL.Path) + } + if r.Method != http.MethodPost { + t.Fatalf("method = %s", r.Method) + } + if err := r.ParseForm(); err != nil { + t.Fatal(err) + } + + params := map[string]string{} + for k, v := range r.PostForm { + if len(v) > 0 { + params[k] = v[0] + } + } + if params["pid"] != "pid-1" { + t.Fatalf("pid = %s", params["pid"]) + } + if params["sign_type"] != "MD5" { + t.Fatalf("sign_type = %s", params["sign_type"]) + } + if !VerifySign(params, "secret") { + t.Fatalf("invalid sign: %s", params["sign"]) + } + + _ = json.NewEncoder(w).Encode(map[string]any{ + "code": 200, + "message": "Success", + "data": map[string]any{ + "order_no": "P123", + "out_trade_no": params["out_trade_no"], + "amount": 100, + "cashier_url": "http://pay.test/cashier/?orderNo=P123", + }, + }) + })) + defer server.Close() + + client, err := NewClient(server.URL, "pid-1", "secret") + if err != nil { + t.Fatal(err) + } + + resp, err := client.Submit(context.Background(), SubmitRequest{ + OutTradeNo: "A100", + Name: "test", + Money: "1.00", + Type: "alipay", + }) + if err != nil { + t.Fatal(err) + } + if resp.OrderNo != "P123" || resp.Amount != 100 { + t.Fatalf("unexpected response: %+v", resp) + } +} diff --git a/cmd/007pay/main.go b/cmd/007pay/main.go new file mode 100644 index 0000000..8119390 --- /dev/null +++ b/cmd/007pay/main.go @@ -0,0 +1,303 @@ +package main + +import ( + "context" + "encoding/json" + "errors" + "flag" + "fmt" + "os" + "strings" + "time" + + pay007 "gitea.s1f.ren/shiran/007pay-sdk.git" +) + +type globalConfig struct { + BaseURL string + PID string + Key string + Timeout time.Duration +} + +func main() { + if err := run(os.Args[1:]); err != nil { + fmt.Fprintln(os.Stderr, "error:", err) + os.Exit(1) + } +} + +func run(args []string) error { + global := flag.NewFlagSet("007pay", flag.ContinueOnError) + global.SetOutput(os.Stderr) + cfg := globalConfig{} + global.StringVar(&cfg.BaseURL, "base-url", envAny("PAY007_BASE_URL", "007PAY_BASE_URL"), "007Pay service base URL") + global.StringVar(&cfg.PID, "pid", envAny("PAY007_PID", "007PAY_PID"), "merchant app pid/token") + global.StringVar(&cfg.Key, "key", envAny("PAY007_KEY", "007PAY_KEY"), "merchant app secret key") + global.DurationVar(&cfg.Timeout, "timeout", 15*time.Second, "HTTP request timeout") + global.Usage = usage + + if err := global.Parse(args); errors.Is(err, flag.ErrHelp) { + return nil + } else if err != nil { + return err + } + rest := global.Args() + if len(rest) == 0 { + usage() + return errors.New("missing command") + } + + cmd, cmdArgs := rest[0], rest[1:] + switch cmd { + case "submit": + return submit(cfg, cmdArgs) + case "query": + return query(cfg, cmdArgs) + case "refund": + return refund(cfg, cmdArgs) + case "orders": + return orders(cfg, cmdArgs) + case "manual": + return manual(cfg, cmdArgs) + case "stats": + return stats(cfg, cmdArgs) + case "sign": + return signParams(cfg, cmdArgs) + case "verify": + return verifyParams(cfg, cmdArgs) + case "help", "-h", "--help": + usage() + return nil + default: + usage() + return fmt.Errorf("unknown command %q", cmd) + } +} + +func newClient(cfg globalConfig) (*pay007.Client, error) { + return pay007.NewClient(cfg.BaseURL, cfg.PID, cfg.Key, pay007.WithTimeout(cfg.Timeout)) +} + +func submit(cfg globalConfig, args []string) error { + fs := flag.NewFlagSet("submit", flag.ContinueOnError) + req := pay007.SubmitRequest{} + fs.StringVar(&req.OutTradeNo, "out-trade-no", "", "merchant order number") + fs.StringVar(&req.Name, "name", "", "order subject") + fs.StringVar(&req.Money, "money", "", "amount in yuan, for example 9.90") + fs.StringVar(&req.Type, "type", "", "channel code, for example alipay or wechat") + fs.StringVar(&req.ClientType, "clienttype", "", "client type: pc or mobile") + fs.StringVar(&req.NotifyURL, "notify-url", "", "merchant async notify URL") + fs.StringVar(&req.ReturnURL, "return-url", "", "merchant sync return URL") + fs.StringVar(&req.Body, "body", "", "order description") + fs.StringVar(&req.Attach, "attach", "", "extra data") + if err := fs.Parse(args); err != nil { + return err + } + + client, err := newClient(cfg) + if err != nil { + return err + } + resp, err := client.Submit(context.Background(), req) + if err != nil { + return err + } + return printJSON(resp) +} + +func query(cfg globalConfig, args []string) error { + fs := flag.NewFlagSet("query", flag.ContinueOnError) + req := pay007.QueryRequest{} + fs.StringVar(&req.TradeNo, "trade-no", "", "007Pay order number") + fs.StringVar(&req.OutTradeNo, "out-trade-no", "", "merchant order number") + if err := fs.Parse(args); err != nil { + return err + } + + client, err := newClient(cfg) + if err != nil { + return err + } + resp, err := client.Query(context.Background(), req) + if err != nil { + return err + } + return printJSON(resp) +} + +func refund(cfg globalConfig, args []string) error { + fs := flag.NewFlagSet("refund", flag.ContinueOnError) + req := pay007.RefundRequest{} + fs.StringVar(&req.TradeNo, "trade-no", "", "007Pay order number") + fs.StringVar(&req.OutTradeNo, "out-trade-no", "", "merchant order number") + fs.StringVar(&req.Money, "money", "", "refund amount in yuan; omit to refund full order") + fs.StringVar(&req.Reason, "reason", "", "refund reason") + if err := fs.Parse(args); err != nil { + return err + } + + client, err := newClient(cfg) + if err != nil { + return err + } + resp, err := client.Refund(context.Background(), req) + if err != nil { + return err + } + return printJSON(resp) +} + +func orders(cfg globalConfig, args []string) error { + fs := flag.NewFlagSet("orders", flag.ContinueOnError) + req := pay007.OrdersRequest{} + status := fs.Int("status", -1, "order status: 0 pending, 1 paid, 2 closed, 3 refunded") + fs.StringVar(&req.OrderNo, "order-no", "", "007Pay or merchant order number") + fs.IntVar(&req.Page, "page", 1, "page number") + fs.IntVar(&req.PageSize, "page-size", 20, "page size") + if err := fs.Parse(args); err != nil { + return err + } + if *status >= 0 { + req.Status = status + } + + client, err := newClient(cfg) + if err != nil { + return err + } + resp, err := client.Orders(context.Background(), req) + if err != nil { + return err + } + return printJSON(resp) +} + +func manual(cfg globalConfig, args []string) error { + fs := flag.NewFlagSet("manual", flag.ContinueOnError) + req := pay007.ManualRequest{} + fs.StringVar(&req.OutTradeNo, "out-trade-no", "", "merchant order number") + fs.StringVar(&req.Name, "name", "", "order subject") + fs.StringVar(&req.Money, "money", "", "amount in yuan") + fs.StringVar(&req.Type, "type", "", "channel code") + fs.StringVar(&req.Attach, "attach", "", "extra data") + if err := fs.Parse(args); err != nil { + return err + } + + client, err := newClient(cfg) + if err != nil { + return err + } + resp, err := client.Manual(context.Background(), req) + if err != nil { + return err + } + return printJSON(resp) +} + +func stats(cfg globalConfig, args []string) error { + fs := flag.NewFlagSet("stats", flag.ContinueOnError) + req := pay007.StatsRequest{} + fs.StringVar(&req.Begin, "begin", "", "begin time: 2006-01-02 15:04:05") + fs.StringVar(&req.End, "end", "", "end time: 2006-01-02 15:04:05") + if err := fs.Parse(args); err != nil { + return err + } + + client, err := newClient(cfg) + if err != nil { + return err + } + resp, err := client.Stats(context.Background(), req) + if err != nil { + return err + } + return printJSON(resp) +} + +func signParams(cfg globalConfig, args []string) error { + if strings.TrimSpace(cfg.Key) == "" { + return errors.New("--key or PAY007_KEY is required") + } + params, err := parseKV(args) + if err != nil { + return err + } + if cfg.PID != "" { + params["pid"] = cfg.PID + } + params["sign_type"] = "MD5" + params["sign"] = pay007.BuildSign(params, cfg.Key) + return printJSON(params) +} + +func verifyParams(cfg globalConfig, args []string) error { + if strings.TrimSpace(cfg.Key) == "" { + return errors.New("--key or PAY007_KEY is required") + } + params, err := parseKV(args) + if err != nil { + return err + } + return printJSON(map[string]any{ + "ok": pay007.VerifySign(params, cfg.Key), + "sign": pay007.BuildSign(params, cfg.Key), + }) +} + +func parseKV(args []string) (map[string]string, error) { + params := map[string]string{} + for _, arg := range args { + k, v, ok := strings.Cut(arg, "=") + if !ok || strings.TrimSpace(k) == "" { + return nil, fmt.Errorf("invalid param %q, expected key=value", arg) + } + params[k] = v + } + return params, nil +} + +func printJSON(v any) error { + data, err := json.MarshalIndent(v, "", " ") + if err != nil { + return err + } + fmt.Println(string(data)) + return nil +} + +func envAny(keys ...string) string { + for _, key := range keys { + if v := strings.TrimSpace(os.Getenv(key)); v != "" { + return v + } + } + return "" +} + +func usage() { + fmt.Fprintf(os.Stderr, `007Pay merchant SDK CLI + +Global flags: + --base-url 007Pay service base URL, or PAY007_BASE_URL + --pid merchant app pid/token, or PAY007_PID + --key merchant app secret key, or PAY007_KEY + --timeout HTTP timeout, default 15s + +Commands: + submit create a payment order + query query and channel-check an order + refund refund an order + orders list merchant orders + manual create a manual paid order + stats query merchant stats + sign sign key=value params + verify verify key=value params with sign=... + +Examples: + 007pay --base-url http://127.0.0.1:8081 --pid app-token --key secret submit --out-trade-no T1001 --name test --money 0.01 --type alipay + 007pay query --out-trade-no T1001 + 007pay sign out_trade_no=T1001 name=test money=0.01 type=alipay +`) +} diff --git a/examples/create_order/main.go b/examples/create_order/main.go new file mode 100644 index 0000000..299009e --- /dev/null +++ b/examples/create_order/main.go @@ -0,0 +1,37 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "log" + "os" + "time" + + pay007 "gitea.s1f.ren/shiran/007pay-sdk.git" +) + +func main() { + client, err := pay007.NewClient( + os.Getenv("PAY007_BASE_URL"), + os.Getenv("PAY007_PID"), + os.Getenv("PAY007_KEY"), + ) + if err != nil { + log.Fatal(err) + } + + resp, err := client.Submit(context.Background(), pay007.SubmitRequest{ + OutTradeNo: "demo-" + time.Now().Format("20060102150405"), + Name: "SDK demo order", + Money: "0.01", + Type: "alipay", + ClientType: "pc", + }) + if err != nil { + log.Fatal(err) + } + + data, _ := json.MarshalIndent(resp, "", " ") + fmt.Println(string(data)) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..eab24a5 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module gitea.s1f.ren/shiran/007pay-sdk.git + +go 1.20 diff --git a/sign.go b/sign.go new file mode 100644 index 0000000..ecfcf47 --- /dev/null +++ b/sign.go @@ -0,0 +1,55 @@ +package pay007 + +import ( + "crypto/md5" + "encoding/hex" + "sort" + "strings" +) + +var ignoredSignKeys = map[string]struct{}{ + "sign": {}, + "sign_type": {}, +} + +// BuildSign creates the 007Pay merchant MD5 signature. +// +// Empty values, sign, and sign_type are ignored. The remaining params are +// sorted by key, joined as "a=1&b=2", appended with the app secret, then MD5 +// hashed and upper-cased. +func BuildSign(params map[string]string, secret string) string { + keys := make([]string, 0, len(params)) + for k, v := range params { + if v == "" { + continue + } + if _, ok := ignoredSignKeys[k]; ok { + continue + } + keys = append(keys, k) + } + sort.Strings(keys) + + var b strings.Builder + for i, k := range keys { + if i > 0 { + b.WriteByte('&') + } + b.WriteString(k) + b.WriteByte('=') + b.WriteString(params[k]) + } + b.WriteString(secret) + + sum := md5.Sum([]byte(b.String())) + return strings.ToUpper(hex.EncodeToString(sum[:])) +} + +// VerifySign checks params["sign"] with the 007Pay signing rule. +func VerifySign(params map[string]string, secret string) bool { + sign := params["sign"] + if sign == "" { + return false + } + return strings.EqualFold(BuildSign(params, secret), sign) +} diff --git a/sign_test.go b/sign_test.go new file mode 100644 index 0000000..c248b7b --- /dev/null +++ b/sign_test.go @@ -0,0 +1,37 @@ +package pay007 + +import "testing" + +func TestBuildSign(t *testing.T) { + params := map[string]string{ + "pid": "1001", + "out_trade_no": "A100", + "name": "test", + "money": "1.00", + "type": "alipay", + "empty": "", + "sign_type": "MD5", + "sign": "ignored", + } + + got := BuildSign(params, "secret") + const want = "59DD6586A5050050EB08A9806C6F501B" + if got != want { + t.Fatalf("BuildSign() = %s, want %s", got, want) + } +} + +func TestVerifySign(t *testing.T) { + params := map[string]string{ + "pid": "1001", + "out_trade_no": "A100", + "name": "test", + "money": "1.00", + "type": "alipay", + } + params["sign"] = BuildSign(params, "secret") + + if !VerifySign(params, "secret") { + t.Fatal("VerifySign() = false, want true") + } +} diff --git a/types.go b/types.go new file mode 100644 index 0000000..21731de --- /dev/null +++ b/types.go @@ -0,0 +1,175 @@ +package pay007 + +import "time" + +const ( + StatusPending = 0 + StatusPaid = 1 + StatusClosed = 2 + StatusRefund = 3 +) + +// SubmitRequest creates a platform order and returns a cashier URL. +type SubmitRequest struct { + OutTradeNo string + Name string + Money string + Type string + ClientType string + NotifyURL string + ReturnURL string + Body string + Attach string +} + +func (r SubmitRequest) params() map[string]string { + return compactParams(map[string]string{ + "out_trade_no": r.OutTradeNo, + "name": r.Name, + "money": r.Money, + "type": r.Type, + "clienttype": r.ClientType, + "notify_url": r.NotifyURL, + "return_url": r.ReturnURL, + "body": r.Body, + "attach": r.Attach, + }) +} + +type SubmitResponse struct { + OrderNo string `json:"order_no"` + OutTradeNo string `json:"out_trade_no"` + Amount int64 `json:"amount"` + CashierURL string `json:"cashier_url"` +} + +type QueryRequest struct { + TradeNo string + OutTradeNo string +} + +func (r QueryRequest) params() map[string]string { + return compactParams(map[string]string{ + "trade_no": r.TradeNo, + "out_trade_no": r.OutTradeNo, + }) +} + +type QueryResponse struct { + TradeNo string `json:"trade_no"` + OutTradeNo string `json:"out_trade_no"` + ChannelTradeNo string `json:"channel_trade_no"` + Amount int64 `json:"amount"` + Status int `json:"status"` + Completed bool `json:"completed"` + ChannelChecked bool `json:"channel_checked"` + PaidAt *time.Time `json:"paid_at"` +} + +type RefundRequest struct { + TradeNo string + OutTradeNo string + Money string + Reason string +} + +func (r RefundRequest) params() map[string]string { + return compactParams(map[string]string{ + "trade_no": r.TradeNo, + "out_trade_no": r.OutTradeNo, + "money": r.Money, + "reason": r.Reason, + }) +} + +type RefundResponse struct { + RefundNo string `json:"refund_no"` + Status int `json:"status"` +} + +type OrdersRequest struct { + OrderNo string + Status *int + Page int + PageSize int +} + +func (r OrdersRequest) params() map[string]string { + params := compactParams(map[string]string{ + "order_no": r.OrderNo, + }) + if r.Status != nil { + params["status"] = itoa(*r.Status) + } + if r.Page > 0 { + params["page"] = itoa(r.Page) + } + if r.PageSize > 0 { + params["page_size"] = itoa(r.PageSize) + } + return params +} + +type OrdersResponse struct { + Total int64 `json:"total"` + List []Order `json:"list"` +} + +type Order struct { + TradeNo string `json:"trade_no"` + OutTradeNo string `json:"out_trade_no"` + Subject string `json:"subject"` + Amount int64 `json:"amount"` + Fee int64 `json:"fee"` + Status int `json:"status"` + ChannelID uint `json:"channel_id"` + ChannelTradeNo string `json:"channel_trade_no"` + ClientType string `json:"client_type"` + IsManual bool `json:"is_manual"` + PaidAt *time.Time `json:"paid_at"` + CreatedAt *time.Time `json:"created_at"` +} + +type ManualRequest struct { + OutTradeNo string + Name string + Money string + Type string + Attach string +} + +func (r ManualRequest) params() map[string]string { + return compactParams(map[string]string{ + "out_trade_no": r.OutTradeNo, + "name": r.Name, + "money": r.Money, + "type": r.Type, + "attach": r.Attach, + }) +} + +type ManualResponse struct { + TradeNo string `json:"trade_no"` + OutTradeNo string `json:"out_trade_no"` + Amount int64 `json:"amount"` + Status int `json:"status"` +} + +type StatsRequest struct { + Begin string + End string +} + +func (r StatsRequest) params() map[string]string { + return compactParams(map[string]string{ + "begin": r.Begin, + "end": r.End, + }) +} + +type StatsResponse struct { + OrderCount int64 `json:"order_count"` + TotalAmount int64 `json:"total_amount"` + TotalFee int64 `json:"total_fee"` + NetAmount int64 `json:"net_amount"` +}