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.xm-iot.cn" defaultAccount = "15065083933" defaultPassword = "adm@3933" ) // 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"` } // cardResumeRequest 是"机卡分离卡复机"的请求体。 type cardResumeRequest struct { CardNo string `json:"card_no"` } // deviceSwitchCardRequest 是"设备切网"的请求体。 type deviceSwitchCardRequest struct { DeviceNo string `json:"device_no"` ICCID string `json:"iccid"` } // deviceOperationRequest 是设备操作(重启/恢复出厂)的请求体。 type deviceOperationRequest struct { DeviceNo string `json:"device_no"` } // 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、card-resume、packages、wallet-balance、package-order、wallet-transactions、device-traffic、device-switch-card、device-reboot、device-reset") cardNo := flag.String("card-no", "", "卡标识,支持 ICCID、虚拟号、MSISDN") deviceNo := flag.String("device-no", "", "设备标识,支持虚拟号、IMEI") iccid := flag.String("iccid", "", "目标卡 ICCID,device-switch-card 时使用") 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, deviceNo: *deviceNo, iccid: *iccid, }) 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 deviceNo string iccid 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 "card-resume": if opts.cardNo == "" { return 0, nil, errors.New("card-resume 需要传入 -card-no") } return c.resumeCard(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)) case "device-traffic": if opts.deviceNo == "" { return 0, nil, errors.New("device-traffic 需要传入 -device-no") } return c.queryDeviceTraffic(ctx, opts.deviceNo) case "device-switch-card": if opts.deviceNo == "" { return 0, nil, errors.New("device-switch-card 需要传入 -device-no") } if opts.iccid == "" { return 0, nil, errors.New("device-switch-card 需要传入 -iccid") } return c.switchDeviceCard(ctx, opts.deviceNo, opts.iccid) case "device-reboot": if opts.deviceNo == "" { return 0, nil, errors.New("device-reboot 需要传入 -device-no") } return c.rebootDevice(ctx, opts.deviceNo) case "device-reset": if opts.deviceNo == "" { return 0, nil, errors.New("device-reset 需要传入 -device-no") } return c.resetDevice(ctx, opts.deviceNo) 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) resumeCard(ctx context.Context, cardNo string) (int, []byte, error) { return c.postJSON(ctx, "/api/open/v1/cards/resume", cardResumeRequest{CardNo: cardNo}) } func (c *client) queryDeviceTraffic(ctx context.Context, deviceNo string) (int, []byte, error) { query := url.Values{} query.Add("device_no", deviceNo) return c.get(ctx, "/api/open/v1/devices/traffic", query) } func (c *client) switchDeviceCard(ctx context.Context, deviceNo string, iccid string) (int, []byte, error) { return c.postJSON(ctx, "/api/open/v1/devices/switch-card", deviceSwitchCardRequest{DeviceNo: deviceNo, ICCID: iccid}) } func (c *client) rebootDevice(ctx context.Context, deviceNo string) (int, []byte, error) { return c.postJSON(ctx, "/api/open/v1/devices/reboot", deviceOperationRequest{DeviceNo: deviceNo}) } func (c *client) resetDevice(ctx context.Context, deviceNo string) (int, []byte, error) { return c.postJSON(ctx, "/api/open/v1/devices/reset", deviceOperationRequest{DeviceNo: deviceNo}) } 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()) }