实现一个 天气查询工具(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)
}