Files
junhong_cmp_fiber/tests/agent_open_api/main.go
huang 93200a9074
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m57s
开放接口完成
2026-05-11 14:26:10 +08:00

402 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"bytes"
"context"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"net/http"
"net/url"
"os"
"sort"
"strconv"
"strings"
"time"
)
const (
defaultBaseURL = "https://cmp-api.boss160.cn"
defaultAccount = "15571055000"
defaultPassword = "Admin@123456"
)
// apiResponse 是文档定义的统一响应外壳data 保持原始 JSON 便于兼容列表或分页结构。
type apiResponse struct {
Code int `json:"code"`
Data json.RawMessage `json:"data"`
Msg string `json:"msg"`
Timestamp string `json:"timestamp"`
}
// walletPackageOrderRequest 是“钱包套餐购买”的请求体。
type walletPackageOrderRequest struct {
CardNos []string `json:"card_nos"`
PackageCode string `json:"package_code"`
}
// client 是第三方调用方视角的独立客户端,不依赖当前项目内部代码。
type client struct {
baseURL string
account string
password string
httpClient *http.Client
}
func main() {
baseURL := flag.String("base-url", envOrDefault("AGENT_OPEN_API_BASE_URL", defaultBaseURL), "接口服务地址")
account := flag.String("account", envOrDefault("AGENT_OPEN_API_ACCOUNT", defaultAccount), "代理账号用户名或手机号")
password := flag.String("password", envOrDefault("AGENT_OPEN_API_PASSWORD", defaultPassword), "代理账号登录密码")
action := flag.String("action", "wallet-balance", "调用动作realname-status、card-status、card-traffic、packages、wallet-balance、package-order、wallet-transactions")
cardNo := flag.String("card-no", "", "卡标识,支持 ICCID、虚拟号、MSISDN")
cardNos := flag.String("card-nos", "", "卡标识列表,多个值用英文逗号分隔")
packageCode := flag.String("package-code", "", "套餐编码")
page := flag.Int("page", 0, "页码,不传或 0 表示省略")
pageSize := flag.Int("page-size", 0, "每页数量,不传或 0 表示省略")
packageType := flag.String("package-type", "", "套餐类型formal 或 addon")
seriesID := flag.Int("series-id", -1, "套餐系列 ID不传或 -1 表示省略")
packageName := flag.String("package-name", "", "套餐名称,模糊搜索")
transactionType := flag.String("transaction-type", "", "交易类型recharge、deduct、refund")
startDate := flag.String("start-date", "", "开始日期,格式 YYYY-MM-DD")
endDate := flag.String("end-date", "", "结束日期,格式 YYYY-MM-DD")
timeout := flag.Duration("timeout", 15*time.Second, "请求超时时间")
flag.Parse()
c := &client{
baseURL: strings.TrimRight(*baseURL, "/"),
account: *account,
password: *password,
httpClient: &http.Client{
Timeout: *timeout,
},
}
ctx := context.Background()
statusCode, responseBody, err := dispatch(ctx, c, runOptions{
action: *action,
cardNo: *cardNo,
cardNos: *cardNos,
packageCode: *packageCode,
page: *page,
pageSize: *pageSize,
packageType: *packageType,
seriesID: *seriesID,
packageName: *packageName,
transactionType: *transactionType,
startDate: *startDate,
endDate: *endDate,
})
if err != nil {
fmt.Fprintf(os.Stderr, "调用失败:%v\n", err)
os.Exit(1)
}
fmt.Printf("HTTP %d\n", statusCode)
printJSON(responseBody)
if statusCode < http.StatusOK || statusCode >= http.StatusMultipleChoices {
os.Exit(1)
}
}
// runOptions 保存命令行参数,避免把主流程写成过长函数。
type runOptions struct {
action string
cardNo string
cardNos string
packageCode string
page int
pageSize int
packageType string
seriesID int
packageName string
transactionType string
startDate string
endDate string
}
func dispatch(ctx context.Context, c *client, opts runOptions) (int, []byte, error) {
switch opts.action {
case "realname-status":
if opts.cardNo == "" {
return 0, nil, errors.New("realname-status 需要传入 -card-no")
}
return c.queryRealnameStatus(ctx, opts.cardNo)
case "card-status":
if opts.cardNo == "" {
return 0, nil, errors.New("card-status 需要传入 -card-no")
}
return c.queryCardStatus(ctx, opts.cardNo)
case "card-traffic":
if opts.cardNo == "" {
return 0, nil, errors.New("card-traffic 需要传入 -card-no")
}
return c.queryCardTraffic(ctx, opts.cardNo)
case "packages":
return c.queryPackages(ctx, packageQuery(opts))
case "wallet-balance":
return c.queryWalletBalance(ctx)
case "package-order":
requestBody, err := buildPackageOrderRequest(opts.cardNos, opts.packageCode)
if err != nil {
return 0, nil, err
}
return c.createWalletPackageOrder(ctx, requestBody)
case "wallet-transactions":
if err := validateDateRange(opts.startDate, opts.endDate); err != nil {
return 0, nil, err
}
return c.queryWalletTransactions(ctx, walletTransactionQuery(opts))
default:
return 0, nil, fmt.Errorf("未知 action%s", opts.action)
}
}
func (c *client) queryRealnameStatus(ctx context.Context, cardNo string) (int, []byte, error) {
query := url.Values{}
query.Add("card_no", cardNo)
return c.get(ctx, "/api/open/v1/cards/realname-status", query)
}
func (c *client) queryCardStatus(ctx context.Context, cardNo string) (int, []byte, error) {
query := url.Values{}
query.Add("card_no", cardNo)
return c.get(ctx, "/api/open/v1/cards/status", query)
}
func (c *client) queryCardTraffic(ctx context.Context, cardNo string) (int, []byte, error) {
query := url.Values{}
query.Add("card_no", cardNo)
return c.get(ctx, "/api/open/v1/cards/traffic", query)
}
func (c *client) queryPackages(ctx context.Context, query url.Values) (int, []byte, error) {
return c.get(ctx, "/api/open/v1/packages", query)
}
func (c *client) queryWalletBalance(ctx context.Context) (int, []byte, error) {
return c.get(ctx, "/api/open/v1/wallet/balance", nil)
}
func (c *client) createWalletPackageOrder(ctx context.Context, body walletPackageOrderRequest) (int, []byte, error) {
return c.postJSON(ctx, "/api/open/v1/wallet/package-orders", body)
}
func (c *client) queryWalletTransactions(ctx context.Context, query url.Values) (int, []byte, error) {
return c.get(ctx, "/api/open/v1/wallet/transactions", query)
}
func (c *client) get(ctx context.Context, path string, query url.Values) (int, []byte, error) {
return c.do(ctx, http.MethodGet, path, query, nil)
}
func (c *client) postJSON(ctx context.Context, path string, body any) (int, []byte, error) {
bodyBytes, err := json.Marshal(body)
if err != nil {
return 0, nil, fmt.Errorf("序列化请求体失败:%w", err)
}
return c.do(ctx, http.MethodPost, path, nil, bodyBytes)
}
func (c *client) do(ctx context.Context, method string, path string, query url.Values, body []byte) (int, []byte, error) {
endpoint, err := url.Parse(c.baseURL)
if err != nil {
return 0, nil, fmt.Errorf("解析 base-url 失败:%w", err)
}
endpoint.Path = strings.TrimRight(endpoint.Path, "/") + path
endpoint.RawQuery = canonicalQuery(query)
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
nonce, err := newNonce()
if err != nil {
return 0, nil, err
}
sign := buildAgentSign(method, path, query, body, timestamp, nonce, c.account, c.password)
var requestBody io.Reader
if len(body) > 0 {
requestBody = bytes.NewReader(body)
}
request, err := http.NewRequestWithContext(ctx, method, endpoint.String(), requestBody)
if err != nil {
return 0, nil, fmt.Errorf("创建 HTTP 请求失败:%w", err)
}
request.Header.Set("Accept", "application/json")
request.Header.Set("X-Agent-Account", c.account)
request.Header.Set("X-Agent-Password", c.password)
request.Header.Set("X-Agent-Timestamp", timestamp)
request.Header.Set("X-Agent-Nonce", nonce)
request.Header.Set("X-Agent-Sign", sign)
if len(body) > 0 {
request.Header.Set("Content-Type", "application/json")
}
response, err := c.httpClient.Do(request)
if err != nil {
return 0, nil, fmt.Errorf("发送 HTTP 请求失败:%w", err)
}
defer response.Body.Close()
responseBody, err := io.ReadAll(response.Body)
if err != nil {
return response.StatusCode, nil, fmt.Errorf("读取响应失败:%w", err)
}
return response.StatusCode, responseBody, nil
}
func buildAgentSign(method string, path string, query url.Values, body []byte, timestamp string, nonce string, account string, password string) string {
bodyHash := sha256.Sum256(body)
signPayload := strings.Join([]string{
strings.ToUpper(method),
path,
canonicalQuery(query),
hex.EncodeToString(bodyHash[:]),
timestamp,
nonce,
account,
}, "\n")
mac := hmac.New(sha256.New, []byte(password))
mac.Write([]byte(signPayload))
return hex.EncodeToString(mac.Sum(nil))
}
func canonicalQuery(query url.Values) string {
if len(query) == 0 {
return ""
}
keys := make([]string, 0, len(query))
for key := range query {
if strings.EqualFold(key, "sign") {
continue
}
keys = append(keys, key)
}
sort.Strings(keys)
parts := make([]string, 0)
for _, key := range keys {
values := append([]string(nil), query[key]...)
sort.Strings(values)
for _, value := range values {
parts = append(parts, url.QueryEscape(key)+"="+url.QueryEscape(value))
}
}
return strings.Join(parts, "&")
}
func newNonce() (string, error) {
bytes := make([]byte, 16)
if _, err := rand.Read(bytes); err != nil {
return "", fmt.Errorf("生成 nonce 失败:%w", err)
}
return strconv.FormatInt(time.Now().UnixNano(), 10) + "-" + hex.EncodeToString(bytes), nil
}
func packageQuery(opts runOptions) url.Values {
query := url.Values{}
addPositiveInt(query, "page", opts.page)
addPositiveInt(query, "page_size", opts.pageSize)
addString(query, "package_type", opts.packageType)
if opts.seriesID >= 0 {
query.Add("series_id", strconv.Itoa(opts.seriesID))
}
addString(query, "package_name", opts.packageName)
return query
}
func walletTransactionQuery(opts runOptions) url.Values {
query := url.Values{}
addPositiveInt(query, "page", opts.page)
addPositiveInt(query, "page_size", opts.pageSize)
addString(query, "transaction_type", opts.transactionType)
addString(query, "start_date", opts.startDate)
addString(query, "end_date", opts.endDate)
return query
}
func addPositiveInt(query url.Values, key string, value int) {
if value > 0 {
query.Add(key, strconv.Itoa(value))
}
}
func addString(query url.Values, key string, value string) {
if value != "" {
query.Add(key, value)
}
}
func buildPackageOrderRequest(cardNos string, packageCode string) (walletPackageOrderRequest, error) {
if strings.TrimSpace(cardNos) == "" {
return walletPackageOrderRequest{}, errors.New("package-order 需要传入 -card-nos")
}
if strings.TrimSpace(packageCode) == "" {
return walletPackageOrderRequest{}, errors.New("package-order 需要传入 -package-code")
}
items := strings.Split(cardNos, ",")
cards := make([]string, 0, len(items))
for _, item := range items {
cardNo := strings.TrimSpace(item)
if cardNo != "" {
cards = append(cards, cardNo)
}
}
if len(cards) == 0 {
return walletPackageOrderRequest{}, errors.New("package-order 的 -card-nos 不能为空")
}
if len(cards) > 100 {
return walletPackageOrderRequest{}, errors.New("package-order 的 -card-nos 最多支持 100 张卡")
}
return walletPackageOrderRequest{
CardNos: cards,
PackageCode: strings.TrimSpace(packageCode),
}, nil
}
func validateDateRange(startDate string, endDate string) error {
if startDate != "" {
if _, err := time.Parse(time.DateOnly, startDate); err != nil {
return fmt.Errorf("start-date 格式必须为 YYYY-MM-DD%w", err)
}
}
if endDate != "" {
if _, err := time.Parse(time.DateOnly, endDate); err != nil {
return fmt.Errorf("end-date 格式必须为 YYYY-MM-DD%w", err)
}
}
return nil
}
func envOrDefault(key string, fallback string) string {
value := strings.TrimSpace(os.Getenv(key))
if value == "" {
return fallback
}
return value
}
func printJSON(body []byte) {
var raw json.RawMessage
if err := json.Unmarshal(body, &raw); err != nil {
fmt.Println(string(body))
return
}
var pretty bytes.Buffer
if err := json.Indent(&pretty, raw, "", " "); err != nil {
fmt.Println(string(body))
return
}
fmt.Println(pretty.String())
}