Files

305 lines
8.0 KiB
Go

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")
fs.StringVar(&req.ExpireTime, "expire-time", "", "order lifetime in minutes or an absolute expiration time (default 10 minutes)")
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
`)
}