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

devtools/2024/12/27 20:52:35/

本文将分享如何借助当下流行的 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 mainimport ("embed""flag""fmt""io""log""net/http""os""github.com/gin-gonic/gin"static "github.com/soulteary/gin-static"
)type Config struct {Domain stringPort   stringToken  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.FSfunc 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// 获取客户端 IPipInfo.ClientIP = c.ClientIP()// 获取 X-Forwarded-For 头信息forwardedFor := c.GetHeader("X-Forwarded-For")if forwardedFor != "" {ipInfo.ForwardedFor = forwardedFor// X-Forwarded-For 可能包含多个 IP,第一个是原始客户端 IPips := strings.Split(forwardedFor, ",")if len(ips) > 0 {ipInfo.RealIP = strings.TrimSpace(ips[0])if len(ips) > 1 {ipInfo.IsProxy = trueipInfo.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 = trueipInfo.ProxyIP = ipInfo.ClientIPipInfo.RealIP = xRealIP}// 检查是否为私有 IPif 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.IPend   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.FSfunc 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] = trueresult = append(result, str)}}return result
}//go:embed public
var EmbedFS embed.FSfunc 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}// 渲染 JSONrenderJSON := 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.ppmy.cn/devtools/145918.html

相关文章

Vue 3 中父子组件的交互与弹框控制:v-model 和事件传递的实践

目录 前言1. Demo2. 基本知识 前言 &#x1f91f; 找工作&#xff0c;来万码优才&#xff1a;&#x1f449; #小程序://万码优才/r6rqmzDaXpYkJZF 原先的父子组件传递已经说过很多知识&#xff0c;推荐阅读 详细分析Vue3中的props用法&#xff08;父传子&#xff09;详细分析V…

基于Spring Boot的图书管理系统

一、系统背景与意义 随着信息技术的飞速发展&#xff0c;图书馆作为知识资源的宝库&#xff0c;其管理和服务方式的现代化显得尤为重要。传统的图书管理系统往往存在操作复杂、效率低下、功能单一等问题&#xff0c;难以满足现代图书馆的多样化需求。因此&#xff0c;开发一个…

Java 集合使用注意事项总结

Java 集合使用注意事项总结 在 Java 开发中&#xff0c;集合是非常常用的数据结构。正确地使用集合可以提高代码的效率和可读性&#xff0c;同时避免一些常见的错误。本文将结合javaguide的内容&#xff0c;总结 Java 集合使用的注意事项。 一、选择合适的集合类型 Java 提供了…

Docker Compose 配置指南

目录 1. Docker Compose 配置1.1 基本配置结构1.2 docker-compose.yml 的各部分1.3 常用配置选项 2. Docker Compose 使用方法2.1 创建 Docker Compose 配置文件2.2 启动服务2.3 查看容器状态2.4 查看服务日志2.5 停止服务2.6 重新构建服务 3. Docker Compose 常用命令3.1 dock…

每日小题打卡

目录 幂次方 手机键盘 简单排序 校庆 性感素数 幂次方 题目描述 对任意正整数 N&#xff0c;计算 X^Nmod233333 的值。 输入格式 共一行&#xff0c;两个整数 X 和 N。 输出格式 共一行&#xff0c;一个整数&#xff0c;表示 X^Nmod233333 的值。 数据范围 1≤…

Linux内核 -- UIO (User-space I/O) 简介与使用笔记

UIO (User-space I/O) 简介 UIO (User-space I/O) 是 Linux 内核提供的一种机制&#xff0c;用于简化设备驱动的开发。它将设备的硬件资源&#xff08;如内存映射、中断等&#xff09;通过简单的接口暴露给用户空间程序&#xff0c;从而使用户可以在用户空间编写复杂的设备逻辑…

内网穿透ubuntu20 docker coplar

sudo apt-get install curl curl -L https://www.cpolar.com/static/downloads/install-release-cpolar.sh | sudo bash ubuntu-base报错 /sbin/init:No such file or directory解决办法 apt install systemd 命令安装即可 cpolar version 1.3 token认证 登录cpolar官网后台…

《信管通低代码信息管理系统开发平台》Windows环境安装说明

1 简介 《信管通低代码信息管理系统应用平台》提供多环境软件产品开发服务&#xff0c;包括单机、局域网和互联网。我们专注于适用国产硬件和操作系统应用软件开发应用。为事业单位和企业提供行业软件定制开发&#xff0c;满足其独特需求。无论是简单的应用还是复杂的系统&…