go练习天气查询cli

81 阅读3分钟

实现一个 天气查询工具(CLI) ,能够通过调用和风天气的免费api实现未来三天天气查询的能力,以下功能说明:

  • 实现生成JWT

    • 读取本地私有key,以及和风天气提供的id,生成JWT。
  • cobra包调用

    • 调用第三方cobra包实现客户输入参数的提示
  • 调用api查询天气

    • 调用api,对查询的json结果进行格式化输出
package main

import (
	"compress/gzip"
	"crypto/ed25519"
	"crypto/x509"
	"encoding/base64"
	"encoding/json"
	"encoding/pem"
	"fmt"
	"io"
	"net/http"
	"os"
	"time"

	"github.com/spf13/cobra"
)

var city string

func main() {
	var rootCmd = &cobra.Command{
		Use:   "myapp",
		Short: "这是一个天气获取小工具",
	}

	var cmdWeather = &cobra.Command{
		Use:   "weather",
		Short: "获取天气信息",
		PreRunE: func(cmd *cobra.Command, args []string) error {
			if city == "" {
				cmd.Help()
				os.Exit(0)
			}
			return nil
		},
		Run: func(cmd *cobra.Command, args []string) {
			token, err := createJWT()
			if err != nil {
				fmt.Println("生成JWT失败:", err)
				return
			}
			// fmt.Println("生成的JWT:", token)
			weitherapirul := "https://devapi.qweather.com/v7/weather/3d"
			geoapiurl := createGeoapiurl(city)
			cityMap := getCityId(geoapiurl, token)
			getWeatherInfo(weitherapirul, token, cityMap)
		},
	}

	cmdWeather.Flags().StringVarP(&city, "city", "c", "", "指定城市")
	rootCmd.AddCommand(cmdWeather)
	if err := rootCmd.Execute(); err != nil {
		fmt.Println("执行命令失败:", err)
		os.Exit(1)
	}
}

// 从 PEM 文件读取私钥
func loadPrivateKey(filePath string) (ed25519.PrivateKey, error) {
	pemFile, err := os.ReadFile(filePath)
	if err != nil {
		return nil, err
	}

	block, _ := pem.Decode(pemFile)
	if block == nil || block.Type != "PRIVATE KEY" {
		return nil, fmt.Errorf("failed to decode PEM block containing private key")
	}

	privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
	if err != nil {
		return nil, err
	}

	return privateKey.(ed25519.PrivateKey), nil
}

// 生成JWT
func createJWT() (string, error) {
	// 定义 Header
	header := map[string]string{
		"alg": "EdDSA",
		"kid": "CN583XKV77",
	}

	// 定义 Payload
	payload := map[string]interface{}{
		"sub": "3J86CH5WCM",
		"iat": time.Now().Add(-30 * time.Second).Unix(), // 当前时间之前的30秒
		"exp": time.Now().Add(2 * time.Minute).Unix(),   // 过期时间为15分钟
	}

	// 将 Header 和 Payload 转换为 JSON
	headerJSON, err := json.Marshal(header)
	if err != nil {
		return "", err
	}
	payloadJSON, err := json.Marshal(payload)
	if err != nil {
		return "", err
	}

	// Base64URL 编码
	headerBase64 := base64.RawURLEncoding.EncodeToString(headerJSON)
	payloadBase64 := base64.RawURLEncoding.EncodeToString(payloadJSON)

	// 从文件读取私钥
	privateKey, err := loadPrivateKey("ed25519-private.pem")
	if err != nil {
		return "", err
	}

	// 生成签名
	signature := ed25519.Sign(privateKey, []byte(headerBase64+"."+payloadBase64))

	// Base64URL 编码签名
	signatureBase64 := base64.RawURLEncoding.EncodeToString(signature)

	// 拼接最终的 Token
	token := headerBase64 + "." + payloadBase64 + "." + signatureBase64
	return token, nil
}

// 发送请求
func sendRequest(url string, token string) (map[string]interface{}, error) {
	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		err = fmt.Errorf("创建请求失败: %v", err)
		return nil, err
	}

	// 设置 Authorization 头
	req.Header.Set("Authorization", "Bearer "+token)

	// 发送请求
	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		err = fmt.Errorf("发送请求失败: %v", err)
		return nil, err
	}
	defer resp.Body.Close()

	// 检查响应的 Content-Encoding 头
	var body io.Reader
	if resp.Header.Get("Content-Encoding") == "gzip" {
		// 解压缩 Gzip 响应体
		gzipReader, err := gzip.NewReader(resp.Body)
		if err != nil {
			err = fmt.Errorf("创建 Gzip 读取器失败: %v", err)
			return nil, err
		}
		defer gzipReader.Close()
		body = gzipReader
	} else {
		body = resp.Body
	}
	// 处理响应
	var responseBody map[string]interface{}
	if err := json.NewDecoder(body).Decode(&responseBody); err != nil {
		err = fmt.Errorf("解析响应失败: %v", err)
		return nil, err
	}

	return responseBody, nil
	// 打印响应
	// fmt.Println("响应:", responseBody)
	// responseJSON, err := json.MarshalIndent(responseBody, "", "  ")
	// if err != nil {
	// 	err = fmt.Errorf("解析响应失败:", err)
	// 	return nil, err
	// }
	// fmt.Println("响应:", string(responseJSON))
}

// 调用api获取城市id
func getCityId(url string, token string) map[string]string {
	responseBody, err := sendRequest(url, token)
	if err != nil {
		fmt.Println("获取城市id失败:", err)
		return nil
	}
	cityMap := extractCityId(responseBody)

	return cityMap
}

// 提取城市id
func extractCityId(responseBody map[string]interface{}) map[string]string {
	cityMap := make(map[string]string)

	if locations, ok := responseBody["location"].([]interface{}); ok {
		for _, loc := range locations {
			if locMap, ok := loc.(map[string]interface{}); ok {
				adm1 := locMap["adm1"].(string)
				adm2 := locMap["adm2"].(string)
				name := locMap["name"].(string)
				id := locMap["id"].(string)

				// 创建键并将 id 转换为 int
				key := fmt.Sprintf("%s-%s-%s", adm1, adm2, name)
				cityMap[id] = key
			}
		}
	}
	return cityMap
}

// 根据查询的城市id,调用api获取天气信息
func getWeatherInfo(url string, token string, cityIds map[string]string) {
	for id, key := range cityIds {
		newUrl := fmt.Sprintf("%s?location=%s", url, id)
		responseBody, err := sendRequest(newUrl, token)
		if err != nil {
			fmt.Println("获取天气信息失败:", err)
			return
		}
		fmt.Printf("城市:%s\n", key)
		extractWeatherInfo(responseBody)
	}
}

// 提取天气信息
func extractWeatherInfo(responseBody map[string]interface{}) {
	if dailys, ok := responseBody["daily"].([]interface{}); ok {
		for _, da := range dailys {
			if locMap, ok := da.(map[string]interface{}); ok {
				date := locMap["fxDate"].(string)
				tqday := locMap["textDay"].(string)
				tqnight := locMap["textNight"].(string)
				fmt.Printf("日期:%s,白天天气:%s,夜晚天气:%s\n", date, tqday, tqnight)
			}
		}
	}
}

func createGeoapiurl(city string) string {
	return fmt.Sprintf("https://geoapi.qweather.com/v2/city/lookup?location=%s", city)
}