使用 AI 辅助开发一个开源 IP 信息查询工具:一

news2025/7/14 2:10:05

本文将分享如何借助当下流行的 AI 工具,一步步完成一个开源项目的开发。

写在前面

在写代码时,总是会遇到一些有趣的机缘巧合。前几天,我在翻看自己之前的开源项目时,又看到了 DDNS 相关的讨论。虽然在 2021 年我写过两篇相对详细的教程:《使用 Nginx 提供 DDNS 服务(前篇)》和《使用 Nginx 提供 DDNS 服务(中篇)》,但总觉得还可以做得更好。

这几天在上海出差上课,本地的网络和算力资源都比较有限。正好借这个机会,快速开发一个轻量的小工具,顺便也回应下之前有朋友问我的问题:在 AI 时代,开发一个简单应用的成本到底有多低?

去年五月份,我写过一篇文章《AI 加持的代码编写实战:快速实现 Nginx 配置格式化工具》,当时使用的是 ChatGPT,这篇文章中,我们来使用代码能力更强的 Anthropic Claude Sonnet 来完成类似的事情。在这篇文章中,我会尽可能使用对“非程序员”友好的方法,尽量避免使用复杂的 IDE。

项目已经在 Github 开源 soulteary/ip-helper,有需要可以自取,如果觉得有帮助的话,别忘了“一键三连”支持一下。

这个开源小工具的交互设计借鉴了 CIP.CC 的 IP 查询工具。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我一直觉得 CIP.CC 是个非常实用的工具。简洁明了,能快速提供有价值的参考信息。它整合了三个不同的 IP 数据源。实在要说缺点的话,如果能够公开数据库的来源和版本就更棒了。不过,在当前国内数据库和数据源要么收费要么需要申请的环境下,这类网站可能终将成为互联网的一段历史。

本文之所以能够成文,感谢好朋友高老师(IPIP)提供基础数据支持这个项目,在战胜了各种侵权事件之后,IPIP 的数据目前应该是毫无疑问的第一梯队了,恭喜!

另外,遗憾的是,目前该网站的“纯 IP 信息查询”以及“使用 Telnet、FTP 等方式查询”功能已经无法使用。

所以在这个项目中,我会根据自己的理解来实现并补充这些功能。

好了,让我们从前端到后端,来折腾出来这个小工具。

第一步:使用多模态模型创建基础 UI 界面

2024 年底,各大模型都在推出“多模态”能力,让 AI 不仅能读懂文字,还能理解图片、音视频。让我们一步步用这些能力来搭建一个实用的工具界面吧。

从界面设计开始

我们可以先让模型帮助我们生成一个简洁的 UI 模块设计图:设计一个网页工具,左右分栏布局,右侧是查询界面。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

然后,把已有的界面截图(Sketch 画一个,或使用你想借鉴的产品界面)丢给模型,提出一个典型的模糊产品需求:用 HTML 和 CSS 实现一个类似的精致界面。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

接下来,我们在新的对话中继续完善布局细节:“使用 CSS 和 HTML 创建一个左右分栏布局,左侧固定 30%,包含 Logo 图片。”

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

好了,界面的设计和代码就都有了,接下来我们需要一个吸引眼球的 Logo。

主视觉 & Logo 设计

这个环节我选择用 Midjourney 来设计:“来一只动感的大熊猫”。关于提示词,你可以自由发挥,创造更酷的版本。如果感兴趣,可以参考我在 2023 年 4 月写的文章《八十行代码实现开源的 Midjourney、Stable Diffusion “咒语”作图工具》

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片优化

生成的图片往往需要进一步调整。你可以用图片编辑软件调整内容、尺寸和格式:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如果你是 macOS 用户,只想调整图片尺寸,用命令行会更快(这里我们把宽度设为 380 像素):

sips -Z 380 /Users/soulteary/Downloads/panda.png --out small-panda.png   
/Users/soulteary/Downloads/panda.png
  /Users/soulteary/Lab/github/ip-helper/small-panda.png

Favicon 制作

别忘了网站还需要一个 Favicon(收藏夹图标)。

我们可以让 AI 基于已有 Logo 设计一个像素化版本:“参考图片,设计一个简单的马赛克版本的 LOGO”。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

完成这些设计后,我们就可以把 AI 生成的代码保存下来,准备进行下一步的整合处理了。

组装 AI 生成的界面素材

组合好的代码素材,得到的界面类似下面这张图。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

对于AI 生成的界面素材,我们该如何组装成一个完整的应用界面呢?方法其实很简单。当你有了多个独立的界面组件后,可以通过以下方式将它们整合起来:

最简单的方式是创建一个新的AI对话,并提供明确的整合需求,比如:“将查询工具组件集成到左右布局面板的右侧区域"。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如果你具备前端开发经验,更推荐手动组合这些代码。这样不仅能优化性能,还能构建出更合理的代码结构,为后续功能扩展打好基础。

我们得到了界面后,接下来就可以来实现基础的后端服务啦。

第二步:完成服务端设计

后端服务的核心任务是获取和解析用户的 IP 信息,并将结果呈现给用户。

按照经典的模块化思路,我们可以把功能划分为以下几个部分:Web 界面渲染模块、IP 信息解析模块、IP 信息 API 接口模块,以及在原始工具基础上新增的多协议支持(包括 Telnet、FTP 等)。

搭建基础服务框架

接下来,我们继续让 AI 助手帮我们生成代码:使用 Gin 实现一个简单的服务,解析命令行参数和环境变量中的端口和域名信息、以及用户口令。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

很快,基础框架代码就准备就绪了。这段代码为我们提供了一个运行在 8080 端口的服务器,支持通过命令行参数或环境变量来配置服务端口和域名,同时具备基于 TOKEN 的用户认证功能。

完成和模版的交互

我们先把前面的前端代码保存到项目的 public/index.template.html 文件中,同时将 Logo 等静态资源文件也放入 public 目录下。同时根据需要优化程序代码,让用户认证和代码交互体验更加自然顺畅。

另外,我们可以搭配使用我在今年年初写的文章《完善 Golang Gin 框架的静态中间件:Gin-Static》中介绍的中间件 soulteary/gin-static。这样不仅能让程序支持单文件发布,还能提升整体性能。如果你想深入了解相关原理,可以参考《深入浅出 Golang 资源嵌入方案:前篇》以及查看 Go-Embed 标签下的系列文章。

package main

import (
	"embed"
	"flag"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"

	"github.com/gin-gonic/gin"
	static "github.com/soulteary/gin-static"
)

type Config struct {
	Domain string
	Port   string
	Token  string
}

// 解析配置参数
func parseConfig() *Config {
	config := &Config{}

	// 解析命令行参数
	flag.StringVar(&config.Port, "port", "", "服务器端口")
	flag.StringVar(&config.Domain, "domain", "", "服务器域名")
	flag.StringVar(&config.Token, "token", "", "API 访问令牌")
	flag.Parse()

	// 尝试从环境变量中获取未设置的内容
	if config.Port == "" {
		config.Port = os.Getenv("SERVER_PORT")
	}
	if config.Domain == "" {
		config.Domain = os.Getenv("SERVER_DOMAIN")
	}
	if config.Token == "" {
		config.Token = os.Getenv("TOKEN")
	}

	// 使用默认值
	if config.Port == "" {
		config.Port = "8080"
	}
	if config.Domain == "" {
		config.Domain = "localhost"
	}
	if config.Token == "" {
		config.Token = ""
		log.Println("提醒:为了提高安全性,可以设置 `TOKEN` 环境变量或 `token` 命令行参数")
	}

	return config
}

// 验证请求中的令牌
func authMiddleware(config *Config) gin.HandlerFunc {
	return func(c *gin.Context) {
		if config.Token != "" {
			token := c.Query("token")
			if token == "" {
				token = c.GetHeader("X-Token")
			}
			if token != config.Token {
				c.JSON(401, gin.H{"error": "无效的认证令牌"})
				c.Abort()
				return
			}
		}
		c.Next()
	}
}

func Get(link string) ([]byte, error) {
	resp, err := http.Get(link)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("服务器返回非200状态码: %d", resp.StatusCode)
	}

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("读取响应内容失败: %v", err)
	}
	return body, nil
}

//go:embed public
var EmbedFS embed.FS

func main() {
	config := parseConfig()

	r := gin.Default()
	r.GET("/health", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"status": "ok",
			"domain": config.Domain,
		})
	})

	r.Use(static.Serve("/", static.LocalFile("./public", false)))

	r.Use(authMiddleware(config))
	r.GET("/", func(c *gin.Context) {
		buf, err := Get(fmt.Sprintf("http://localhost:%s/index.template.html", config.Port))
		if err != nil {
			c.String(500, "读取模板文件失败: %v", err)
			return
		}
		c.Data(200, "text/html; charset=utf-8", buf)
	})

	serverAddr := fmt.Sprintf(":%s", config.Port)
	log.Printf("启动服务器于 %s:%s\n", "config.Domain", config.Port)
	if err := r.Run(serverAddr); err != nil {
		log.Fatalf("启动服务器失败: %v", err)
	}
}

IP 获取和基础分析功能的实现

在与模型的进一步对话中,我们实现更核心的功能:使用 Golang Gin 框架来获取用户访问时的 IP 信息,并判断请求是否经过了代理服务器。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

获得这段代码后,我们将它与之前的功能进行整合(新增代码有注释)。现在,我们的服务不仅可以获取用户的 IP 信息,还能够基础地判断请求是否通过代理服务器转发。

除了网页展示外,我们还新增了一个 /ip 接口,让用户可以直接通过程序获取纯 IP 信息,提供了更灵活的使用方式。

// ...

// IPInfo 存储 IP 相关信息
type IPInfo struct {
	ClientIP     string `json:"client_ip"`
	ProxyIP      string `json:"proxy_ip,omitempty"`
	IsProxy      bool   `json:"is_proxy"`
	ForwardedFor string `json:"forwarded_for,omitempty"`
	RealIP       string `json:"real_ip"`
}

// 获取并分析 IP 信息的中间件
func IPAnalyzer() gin.HandlerFunc {
	return func(c *gin.Context) {
		ipInfo := analyzeIP(c)
		// 将 IP 信息存储到上下文中
		c.Set("ip_info", ipInfo)
		c.Next()
	}
}

// 分析 IP 信息
func analyzeIP(c *gin.Context) IPInfo {
	var ipInfo IPInfo

	// 获取客户端 IP
	ipInfo.ClientIP = c.ClientIP()

	// 获取 X-Forwarded-For 头信息
	forwardedFor := c.GetHeader("X-Forwarded-For")
	if forwardedFor != "" {
		ipInfo.ForwardedFor = forwardedFor
		// X-Forwarded-For 可能包含多个 IP,第一个是原始客户端 IP
		ips := strings.Split(forwardedFor, ",")
		if len(ips) > 0 {
			ipInfo.RealIP = strings.TrimSpace(ips[0])
			if len(ips) > 1 {
				ipInfo.IsProxy = true
				ipInfo.ProxyIP = strings.TrimSpace(ips[len(ips)-1])
			}
		}
	} else {
		ipInfo.RealIP = ipInfo.ClientIP
	}

	// 获取 X-Real-IP 头信息
	xRealIP := c.GetHeader("X-Real-IP")
	if xRealIP != "" && xRealIP != ipInfo.RealIP {
		ipInfo.IsProxy = true
		ipInfo.ProxyIP = ipInfo.ClientIP
		ipInfo.RealIP = xRealIP
	}

	// 检查是否为私有 IP
	if isPrivateIP(ipInfo.ClientIP) {
		ipInfo.IsProxy = true
	}

	return ipInfo
}

// 检查是否为私有 IP 地址
func isPrivateIP(ipStr string) bool {
	ip := net.ParseIP(ipStr)
	if ip == nil {
		return false
	}

	// 检查是否为私有 IP 范围
	privateIPRanges := []struct {
		start net.IP
		end   net.IP
	}{
		{net.ParseIP("10.0.0.0"), net.ParseIP("10.255.255.255")},
		{net.ParseIP("172.16.0.0"), net.ParseIP("172.31.255.255")},
		{net.ParseIP("192.168.0.0"), net.ParseIP("192.168.255.255")},
	}

	for _, r := range privateIPRanges {
		if bytes.Compare(ip, r.start) >= 0 && bytes.Compare(ip, r.end) <= 0 {
			return true
		}
	}
	return false
}

//go:embed public
var EmbedFS embed.FS

func main() {
	// ...

	r.Use(static.Serve("/", static.LocalFile("./public", false)))

	r.Use(authMiddleware(config))
	// 使用IP分析中间件
	r.Use(IPAnalyzer())
	r.GET("/", func(c *gin.Context) {
		// 先获取 IP 信息
		ipInfo, exists := c.Get("ip_info")
		if !exists {
			c.JSON(500, gin.H{"error": "IP info not found"})
			return
		}

		buf, err := Get(fmt.Sprintf("http://localhost:%s/index.template.html", config.Port))
		if err != nil {
			c.String(500, "读取模板文件失败: %v", err)
			return
		}

		// TODO 将 IP 信息传递给模板
		fmt.Println(ipInfo)

		c.Data(200, "text/html; charset=utf-8", buf)
	})
	// 单独提供一个接口,来获取 IP 信息
	r.GET("/ip", func(c *gin.Context) {
		ipInfo, exists := c.Get("ip_info")
		if !exists {
			c.JSON(500, gin.H{"error": "IP info not found"})
			return
		}
		c.JSON(200, ipInfo)
	})

	serverAddr := fmt.Sprintf(":%s", config.Port)
	log.Printf("启动服务器于 %s:%s\n", "config.Domain", config.Port)
	if err := r.Run(serverAddr); err != nil {
		log.Fatalf("启动服务器失败: %v", err)
	}

	r.Run(":8080")
}

启动程序后,我们可以通过命令行或者直接在浏览器中访问 http://localhost:8080/ip 来测试功能。比如使用 curl 命令:

# curl 127.0.0.1:8080/ip

{"client_ip":"127.0.0.1","is_proxy":false,"real_ip":"127.0.0.1"}

看到这个返回结果,说明我们的基础功能已经正常运行了。

接下来,我们先不着急处理模板渲染的部分,而是把注意力放在 IP 信息和数据库对接这个核心模块上。

完成 IP 数据库查询功能

在2020年时,因业务需求我曾使用过高老师的 IP 库(通过阿里云购买),并写过两篇关于如何处理本地数据的高性能方案文章:《阿里云 IP 地理位置库(淘宝IP库)实践(前篇)》和《阿里云 IP 地理位置库(淘宝IP库)实践(后篇)》。

这次为了开发这个小工具,我向高老师获取了精简版数据和解析文档。由于我只需要像文章开头提到的那样解析基础地理信息,所以我选择 fork 了一个 Go SDK 并进行了简化处理。

这次为了完成这个小工具,和高老师要来了精简版的数据,以及解析文档,因为我只想和文章开头一样,解析出基础的地理信息,所以我 fork 了一个 Go SDK 版本,并做了 “青春版化” 处理。

首先,在项目目录中执行以下命令来下载简化版 SDK:

go get github.com/soulteary/ipdb-go

接下来,我们将在之前的代码基础上添加查询功能,并新增一个 /ip/:ip 路由,让用户可以查询指定 IP 的数据。

// ...

// 帮助我们对数据库中的内容进行去重
// eg: ["CLOUDFLARE.COM","CLOUDFLARE.COM",""] => ["CLOUDFLARE.COM",""]

func removeDuplicates(strSlice []string) []string {
	// 创建一个 map 用于存储唯一的字符串
	encountered := make(map[string]bool)
	result := []string{}

	// 遍历切片,将未出现过的字符串添加到结果中
	for _, str := range strSlice {
		if !encountered[str] {
			encountered[str] = true
			result = append(result, str)
		}
	}

	return result
}

//go:embed public
var EmbedFS embed.FS

func main() {
	config := parseConfig()

	// 初始化 IP 数据库
	db, err := ipdb.NewCity("./data/ipipfree.ipdb")
	if err != nil {
		log.Fatal(err)
	}
	// 更新 ipdb 文件后可调用 Reload 方法重新加载内容
	// db.Reload("./data/ipipfree.ipdb")

	r := gin.Default()
	r.GET("/health", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"status": "ok",
			"domain": config.Domain,
		})
	})

	r.Use(static.Serve("/", static.LocalFile("./public", false)))

	r.Use(authMiddleware(config))
	r.Use(IPAnalyzer())
	r.GET("/", func(c *gin.Context) {
		ipInfo, exists := c.Get("ip_info")
		if !exists {
			c.JSON(500, gin.H{"error": "IP info not found"})
			return
		}

		buf, err := Get(fmt.Sprintf("http://localhost:%s/index.template.html", config.Port))
		if err != nil {
			c.String(500, "读取模板文件失败: %v", err)
			return
		}

		// TODO 将 IP 信息传递给模板
		fmt.Println(ipInfo)

		c.Data(200, "text/html; charset=utf-8", buf)
	})
	// 获取当前请求方的 IP 地址信息
	r.GET("/ip", func(c *gin.Context) {
		ipInfo, exists := c.Get("ip_info")
		if !exists {
			c.JSON(500, gin.H{"error": "IP info not found"})
			return
		}
		c.JSON(200, ipInfo)
	})
	// 获取指定 IP 地址信息
	r.GET("/ip/:ip", func(c *gin.Context) {
		// 获取 URL 中的 IP 地址
		ipaddr := c.Param("ip")
		fmt.Println("ip", ipaddr)
		if ipaddr == "" {
			ipInfo, exists := c.Get("ip_info")
			if !exists {
				c.JSON(500, gin.H{"error": "IP info not found"})
				return
			}
			ipaddr = ipInfo.(IPInfo).RealIP
		}

		dbInfo, err := db.Find(ipaddr, "CN")
		if err != nil {
			dbInfo = []string{"未找到 IP 地址信息"}
		}
		dbInfo = removeDuplicates(dbInfo)
		c.JSON(200, map[string]any{"ip": ipaddr, "info": dbInfo})
	})

	serverAddr := fmt.Sprintf(":%s", config.Port)
	log.Printf("启动服务器于 %s:%s\n", "config.Domain", config.Port)
	if err := r.Run(serverAddr); err != nil {
		log.Fatalf("启动服务器失败: %v", err)
	}

	r.Run(":8080")
}

让我们通过命令行或浏览器来验证服务是否正常运行。我们可以测试几个不同的 IP 地址:

首先测试获取当前请求来源的 IP 信息。

# curl 127.0.0.1:8080/ip

{"client_ip":"127.0.0.1","is_proxy":false,"real_ip":"127.0.0.1"}

然后测试查询特定IP地址。

# curl 127.0.0.1:8080/ip/123.123.123.123

{"info":["中国","北京"],"ip":"123.123.123.123"}

最后测试一个 CloudFlare 的 IP:

# curl 127.0.0.1:8080/ip/1.1.1.1

{"info":["CLOUDFLARE.COM",""],"ip":"1.1.1.1"}

第三步:从静态页面到动态网站,数据与界面的整合

我们已经完成了基础架构的搭建工作,现在要进入最后也是最关键的阶段:将数据层和展示层打通,让整个系统真正运转起来。让我们一步步来实现这个目标。

模版和服务数据联动

第一步,我们需要改造之前的静态模板。我们要把原本写死的数据替换成程序可以动态填充的占位符:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

接下来,我们先实现一个基础版本的IP信息查询功能:当用户访问网站首页时,系统会自动获取访问者的IP地址,并展示相关的IP信息。

// ...
func main() {
	// ...
	r.GET("/", func(c *gin.Context) {
		ipInfo, exists := c.Get("ip_info")
		if !exists {
			c.JSON(500, gin.H{"error": "IP info not found"})
			return
		}

		// 查询 IP 地址具体信息
		dbInfo, err := db.Find(ipInfo.(IPInfo).RealIP, "CN")
		if err != nil {
			dbInfo = []string{"未找到 IP 地址信息"}
		}
		// 读取默认模版
		template, err := Get(fmt.Sprintf("http://localhost:%s/index.template.html", config.Port))
		if err != nil {
			c.String(500, "读取模板文件失败: %v", err)
			return
		}
		// 更新模版中的 IP 地址
		template = bytes.ReplaceAll(template, []byte("%IP_ADDR%"), []byte(ipInfo.(IPInfo).ClientIP))
		// 更新模版中的域名
		template = bytes.ReplaceAll(template, []byte("%DOMAIN%"), []byte(config.Domain))
		// 更新模版中的 IP 地址信息
		template = bytes.ReplaceAll(template, []byte("%DATA_1_INFO%"), []byte(strings.Join(removeDuplicates(dbInfo), " ")))

		c.Data(200, "text/html; charset=utf-8", template)
	})
// ...
}

完成模板更新后,我们需要启动服务来验证功能。使用以下命令启动:

SERVER_DOMAIN=localhost:8080 go run main.go

启动服务后,打开浏览器访问 localhost:8080,我们就可以看到如下界面:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

从界面可以看到,页面的数据联动功能已经正常工作。不过目前使用的数据库还不支持 IPv6 地址的查询(需要使用商业版本或增加其他数据库),导致部分信息展示不符合预期。没关系,接下来我们就来实现按指定 IP 查询的功能。

后端处理前端用户输入

为了让用户能够与我们的应用进行交互,现在让我们对之前的静态 HTML 模板做一些优化。我们将添加一个表单来处理用户输入的 IP 地址。

首先,在 HTML 模版中添加数据表单:

<div class="search-container">
  <form action="/" method="post">
    <input type="text" name="ip" class="search-input" placeholder="请输入要查询的 IP 地址" value="%IP_ADDR%" />
    <button class="search-button" type="submit">查询</button>  
  </form>
</div>

在这段代码中:

  • 使用 form 标签创建表单,设置 action="/" 将数据提交到根路径
  • method="post" 指定使用 POST 方法提交数据
  • 输入框中的 value="%IP_ADDR%" 用于回显用户之前输入的 IP 地址

接下来,我们需要在后端添加相应的处理逻辑:

// ...
// 使用 net 包验证 IP 地址
func isValidIPAddress(ip string) bool {
	if parsedIP := net.ParseIP(ip); parsedIP != nil {
		return true
	}
	return false
}

// IPForm 定义表单结构
type IPForm struct {
	IP string `form:"ip" binding:"required"`
}

func main() {
	// ...
	// 处理 POST 请求,解析表单数据
	r.POST("/", func(c *gin.Context) {
		// 获取请求中的 IP 地址信息
		ipInfo, exists := c.Get("ip_info")
		if !exists {
			c.JSON(500, gin.H{"error": "IP info not found"})
			return
		}
		// 默认 IP 地址为空
		ip := ""
		var form IPForm
		// 使用 ShouldBind 绑定表单数据
		if err := c.ShouldBind(&form); err != nil {
			// 如果绑定失败,使用请求中的 IP 地址
			ip = ipInfo.(IPInfo).RealIP
		} else {
			// 获取到 IP 地址后的处理逻辑
			ip = form.IP
			// 如果 IP 地址不合法,使用请求中的 IP 地址
			if !isValidIPAddress(ip) {
				ip = ipInfo.(IPInfo).RealIP
			}
		}
		c.Redirect(302, fmt.Sprintf("/ip/%s", ip))
	})
// ...
}

程序首先会记录发起请求的客户端 IP。然后检查用户通过表单提交的 IP 地址是否正确。如果 IP 地址正确,会自动跳转到类似 /ip/123.123.123.123 这样的地址来展示 IP 详细信息。如果提交的 IP 地址无效,则会使用客户端的实际 IP 地址进行跳转。

打造统一的接口,适配多种场景

细心的朋友可能注意到了,前面提到的 /ip/:ip 接口原本是为命令行工具设计的,默认返回 JSON 格式数据,而不是网页界面。在 CIP 网站的设计中,浏览器访问和命令行调用使用了不同的接口地址。不过通过一些技巧,我们完全可以让同一个接口同时支持这两种使用场景。

先来将 IP 获取和信息查询,以及渲染部分分别抽象为独立的模块:

// ...
func main() {
	//...
	// 获取客户端 IP 信息
	getClientIPInfo := func(c *gin.Context, ipaddr string) (resultIP string, resultDBInfo []string, err error) {
		// 判断是否有传入 IP 地址
		if ipaddr == "" {
			// 如果没有有效 IP,默认使用发起请求的客户端 IP 信息
			ipInfo, exists := c.Get("ip_info")
			if !exists {
				return resultIP, resultDBInfo, fmt.Errorf("IP info not found")
			}
			ipaddr = ipInfo.(IPInfo).RealIP
		}

		dbInfo, err := db.Find(ipaddr, "CN")
		if err != nil {
			dbInfo = []string{"未找到 IP 地址信息"}
		}
		dbInfo = removeDuplicates(dbInfo)
		return ipaddr, dbInfo, nil
	}

	// 渲染模板
	renderTemplate := func(globalTemplate []byte, ipaddr string, dbInfo []string) []byte {
		template := bytes.ReplaceAll(globalTemplate, []byte("%IP_ADDR%"), []byte(ipaddr))
		template = bytes.ReplaceAll(template, []byte("%DOMAIN%"), []byte(config.Domain))
		template = bytes.ReplaceAll(template, []byte("%DATA_1_INFO%"), []byte(strings.Join(removeDuplicates(dbInfo), " ")))
		return template
	}

	// 渲染 JSON
	renderJSON := func(ipaddr string, dbInfo []string) map[string]any {
		return map[string]any{"ip": ipaddr, "info": dbInfo}
	}

	globalTemplate := []byte{}

	r.GET("/", func(c *gin.Context) {
		// 预缓存模板文件
		if len(globalTemplate) == 0 {
			globalTemplate, err = Get(fmt.Sprintf("http://localhost:%s/index.template.html", config.Port))
			if err != nil {
				log.Fatalf("读取模板文件失败: %v\n", err)
				return
			}
		}

		// 获取客户端 IP 信息,首页不需要传入 IP 地址
		ipAddr, dbInfo, err := getClientIPInfo(c, "")
		if err != nil {
			c.JSON(500, gin.H{"error": err.Error()})
			return
		}
		// 返回渲染后的 HTML 内容
		c.Data(200, "text/html; charset=utf-8", renderTemplate(globalTemplate, ipAddr, dbInfo))
	})

	r.GET("/ip/:ip", func(c *gin.Context) {
		ip := c.Param("ip")
		// 获取指定 IP 地址的信息
		ipAddr, dbInfo, err := getClientIPInfo(c, ip)
		if err != nil {
			c.JSON(500, gin.H{"error": err.Error()})
			return
		}
		c.JSON(200, renderJSON(ipAddr, dbInfo))
	})
    // ...
}

接下来,我们要实现一个功能:自动识别访问请求是来自类似 curl 的命令行工具,还是来自浏览器。

// ...
// 判断请求发起方是否为“下载工具”
func IsDownloadTool(userAgent string) bool {
	// 转换为小写以便不区分大小写比较
	ua := strings.ToLower(userAgent)

	// 常见下载工具的特征字符串
	downloadTools := []string{
		"curl",
		"wget",
		"aria2",
		"python-requests",
		"axios",
		"got",
		"postman",
	}

	for _, tool := range downloadTools {
		if strings.Contains(ua, tool) {
			return true
		}
	}

	return false
}

func main() {
	// ...
	r.GET("/", func(c *gin.Context) {
		if len(globalTemplate) == 0 {
			globalTemplate, err = Get(fmt.Sprintf("http://localhost:%s/index.template.html", config.Port))
			if err != nil {
				log.Fatalf("读取模板文件失败: %v\n", err)
				return
			}
		}

		ipAddr, dbInfo, err := getClientIPInfo(c, "")
		if err != nil {
			c.JSON(500, gin.H{"error": err.Error()})
			return
		}

		// 获取请求头中的 User-Agent 信息
		userAgent := c.GetHeader("User-Agent")
		// 使用下载工具访问时返回 JSON 格式
		if IsDownloadTool(userAgent) {
			c.JSON(200, renderJSON(ipAddr, dbInfo))
		} else {
			c.Data(200, "text/html; charset=utf-8", renderTemplate(globalTemplate, ipAddr, dbInfo))
		}
	})

	r.GET("/ip/:ip", func(c *gin.Context) {
		ip := c.Param("ip")
		ipAddr, dbInfo, err := getClientIPInfo(c, ip)
		if err != nil {
			c.JSON(500, gin.H{"error": err.Error()})
			return
		}
		// 获取请求头中的 User-Agent 信息
		userAgent := c.GetHeader("User-Agent")
		// 使用下载工具访问时返回 JSON 格式
		if IsDownloadTool(userAgent) {
			c.JSON(200, renderJSON(ipAddr, dbInfo))
		} else {
			c.Data(200, "text/html; charset=utf-8", renderTemplate(globalTemplate, ipAddr, dbInfo))
		}
	})

	// ...
}

经过上面的改进,不管是访问根路径 / 还是 /ip/:ip 接口,程序都能根据访问方式自动返回合适的格式。浏览器访问会看到格式化的页面,命令行工具访问则获得纯文本结果。这样一来,我们其实可以考虑是否要保留之前专门为命令行工具设计的 /ip 接口,因为现在 / 已经能够处理这两种场景了。当然,如果特别在意性能,保留专门的接口也是一种选择。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

和之前一样,重启程序后,我们可以打开浏览器做个简单测试。随便输入一个 IP 地址进行查询,你会发现一切都在按照预期正常运行。

最后

到这里,我们已经实现了这个应用的核心功能。在下一篇文章中,我们将继续探讨本文中的一些遗留问题,看看如何借助 AI 的力量来帮助我们更快地完成应用开发。

–EOF


我们有一个小小的折腾群,里面聚集了一些喜欢折腾、彼此坦诚相待的小伙伴。

我们在里面会一起聊聊软硬件、HomeLab、编程上、生活里以及职场中的一些问题,偶尔也在群里不定期的分享一些技术资料。

关于交友的标准,请参考下面的文章:

致新朋友:为生活投票,不断寻找更好的朋友

当然,通过下面这篇文章添加好友时,请备注实名和公司或学校、注明来源和目的,珍惜彼此的时间 😄

关于折腾群入群的那些事


如果你觉得内容还算实用,欢迎点赞分享给你的朋友,在此谢过。

如果你想更快的看到后续内容的更新,请戳 “点赞”、“分享”、“在看” ,这些免费的鼓励将会影响后续有关内容的更新速度。


本文使用「署名 4.0 国际 (CC BY 4.0)」许可协议,欢迎转载、或重新修改使用,但需要注明来源。 署名 4.0 国际 (CC BY 4.0)

本文作者: 苏洋

创建时间: 2024年12月21日
统计字数: 17445字
阅读时间: 35分钟阅读
本文链接: https://soulteary.com/2024/12/21/use-ai-to-assist-in-developing-an-open-source-ip-information-tool-part-1.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2264179.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

门控循环单元(GRU):深度学习中的序列数据处理利器

目录 ​编辑 引言 GRU的诞生背景 GRU的核心机制 GRU的计算过程 GRU的数学公式 GRU的应用领域 代码示例&#xff1a;PyTorch中的GRU GRU与LSTM的比较 参数比较 GRU的技术发展 BiGRU&#xff08;双向GRU&#xff09; BiGRU的实现示例 GRU与CNN的结合 GRU的应用案例…

Sui 基金会任命 Christian Thompson 为新任负责人

Sui 基金会是专注于推动 Sui 蓬勃发展的生态增长与采用的机构。近日&#xff0c;基金会宣布任命 Christian Thompson 为新任负责人。在 Sui 主网发布的开创性一年里&#xff0c;Sui 凭借其无与伦比的速度、可扩展性和效率&#xff0c;迅速崛起为领先的 Layer 1 区块链之一&…

Vue2五、商品分类:My-Tag表头组件,My-Table整个组件

准备&#xff1a; 安包 npm less less-loader。拆分&#xff1a;一共分成两个组件部分&#xff1a; 1&#xff1a;My-Tag 标签一个组件。2&#xff1a;My-Table 整体一个组件&#xff08;表头不固定&#xff0c;内容不固定&#xff08;插槽&#xff09;&#xff09; 一&…

mysql运维篇笔记——日志,主从复制,分库分表,读写分离

目录 日志 错误日志 二进制日志 查询日志 慢查询日志 主从复制 概念&#xff1a; 优点&#xff1a; 原理&#xff1a; 搭建&#xff1a; 1&#xff0c;服务器准备 2&#xff0c;主库配置 3&#xff0c;从库配置 4&#xff0c;测试 分库分表&#xff1a; 介绍 问题分析 中心思想…

【JavaEE初阶】线程 和 thread

本节⽬标 认识多线程 掌握多线程程序的编写 掌握多线程的状态 一. 认识线程&#xff08;Thread&#xff09; 1概念 1) 线程是什么 ⼀个线程就是⼀个 "执⾏流". 每个线程之间都可以按照顺序执⾏⾃⼰的代码. 多个线程之间 "同时" 执⾏着多份代码. 还…

设计模式期末复习

一、设计模式的概念以及分类 二、设计模式的主题和意图 设计模式的主题是关于软件设计中反复出现的问题以及相应的解决方案。这些主题是基于长期实践经验的总结&#xff0c;旨在提供一套可复用的设计思路和框架&#xff0c;以应对软件开发中的复杂性和变化性。 三、面向对象程…

【小白51单片机专用教程】protues仿真AT89C51入门

课程特点 无需开发板0基础教学软件硬件双修辅助入门 本课程面对纯小白&#xff0c;因此会对各个新出现的知识点在实例基础上进行详细讲解&#xff0c;有相关知识的可以直接跳过。课程涉及protues基本操作、原理图设计、数电模电、kell使用、C语言基本内容&#xff0c;所有涉及…

MFC用List Control 和Picture控件实现界面切换效果

添加List Control 和Picture控件 添加 3个子窗体 把子窗体边框设置为None, 样式设为Child 声明 CListCtrl m_listPageForm;void ShowForm(int nIndex);void CreatFormList();void CMFCApplication3Dlg::DoDataExchange(CDataExchange* pDX) {CDialogEx::DoDataExchange(pDX);DD…

Linux高并发服务器开发 第五天(压缩解压缩/vim编辑器)

目录 1.压缩和解压缩 1.1压缩 1.2解压缩 2.vim编辑器 2.1vim的3种工作模式 2.2切换编辑模式 2.3保存和退出 2.4光标移动 1.压缩和解压缩 - Linux 操作系统&#xff0c;默认支持的 压缩格式&#xff1a;gzip、bzip2。 默认&#xff0c;这两种压缩格式&#xff0c;只能…

接口测试Day-02-安装postman项目推送Gitee仓库

postman安装 下载 Postman&#xff08;已提供安装包&#xff0c;此步可以跳过&#xff09; https://www.postman.com/downloads/安装 Postman 安装Postman插件newman 要想给 postman 安装 newman 插件&#xff0c;必须 先 安装 node.js。 这是前提&#xff01; 安装node.js 可能…

虚拟地址空间 -- 虚拟地址,虚拟内存管理

1. C/C语言的内存空间分布 用下列代码来观察各种区域的地址&#xff1a; #include <stdio.h> #include <unistd.h> #include <stdlib.h>int g_unval; int g_val 100;int main(int argc, char *argv[], char *env[]) {const char *str "helloworld&qu…

【数字化】华为数字化转型架构蓝图-2

目录 1、客户联结的架构思路 1.1 ROADS体验设计 1.2 具体应用场景 1.3 统一的数据底座 1.4 案例与成效 2、一线作战平台的架构思路 2.1 核心要素 2.2 关键功能 2.3 实施路径 2.4 案例与成效 3、能力数字化的架构思路 3.1 能力数字化的核心目标 3.2 能力数字化的实…

【优选算法】—移动零(双指针算法)

云边有个稻草人-CSDN博客 想当一名牛的程序员怎么能少的了练习算法呢&#xff1f;&#xff01; 今天就立即开启一个新专栏&#xff0c;专干算法&#xff0c;提高算法能力&#xff08;废柴的我也在准备蓝桥杯哈哈&#xff09;—— 目录 1.【 283. 移动零 - 力扣&#xff08;Lee…

AI的进阶之路:从机器学习到深度学习的演变(三)

&#xff08;承接上集&#xff1a;AI的进阶之路&#xff1a;从机器学习到深度学习的演变&#xff08;二&#xff09;&#xff09; 四、深度学习&#xff08;DL&#xff09;&#xff1a;机器学习的革命性突破 深度学习&#xff08;DL&#xff09;作为机器学习的一个重要分支&am…

Python自动化测试:线上流量回放

&#x1f345; 点击文末小卡片&#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 在自动化测试中&#xff0c;线上流量回放是一项关键技术&#xff0c;可以模拟真实用户的请求并重现线上场景&#xff0c;验证系统的性能和稳定性。本文将介绍Pytho…

初始C语言3

目录 9. 操作符 9.1 算术操作符 9.2 移位操作符 9.3 位操作符 9.4 赋值操作符 9.5 单目操作符 9.6 关系操作符 9.7 逻辑操作符 9.8 条件操作符 9.9 逗号表达式 下标引用、函数调用和结构成员 10. 常见关键字 10.1 typedef 10.2 static 10.2.1 修饰局部变量 10.…

【Rust自学】4.5. 切片(Slice)

4.5.0. 写在正文之前 这是第四章的最后一篇文章了&#xff0c;在这里也顺便对这章做一个总结&#xff1a; 所有权、借用和切片的概念确保 Rust 程序在编译时的内存安全。 Rust语言让程序员能够以与其他系统编程语言相同的方式控制内存使用情况&#xff0c;但是当数据所有者超…

VPN技术-GRE隧道的配置

GRE隧道的配置 1&#xff0c; 在AR1上配置DHCP接口地址池&#xff0c;AR3上配置DHCP全局地址池 2&#xff0c; PC1获取的IP地址为10.10.10.253&#xff0c;PC2获取的IP地址为10.10.30.253 3&#xff0c;通过ip route-static将目的地址为10.10.30.253的流量引入到Tunnel #配…

碰撞检测算法之闵可夫斯基差集法(Minkowski Difference)

在游戏开发和机器人路径规划乃至于现在比较火的自动驾驶中&#xff0c;我们常常需要确定两个物体是否发生碰撞&#xff0c;有一种通过闵可夫斯基差集法求是否相交的算法&#xff0c;下面将介绍一下 闵可夫斯基差集法的优势 闵可夫斯基差集法优势&#xff1a; 可以处理复杂的…

Python OCR 文字识别

一.引言 文字识别&#xff0c;也称为光学字符识别&#xff08;Optical Character Recognition, OCR&#xff09;&#xff0c;是一种将不同形式的文档&#xff08;如扫描的纸质文档、PDF文件或数字相机拍摄的图片&#xff09;中的文字转换成可编辑和可搜索的数据的技术。随着技…