ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

从 PHP 到 AI + Golang,程序员自救转型手记(十九):点选验证码初版落地,TaoToken 统一 Key 打通随机数与文件系统包

从 PHP 到 AI + Golang,程序员自救转型手记(十九):点选验证码初版落地,TaoToken 统一 Key 打通随机数与文件系统包 1. 从 PHP 到 Golang点选验证码初版落地踩了哪些坑点选验证码是什么简单说就是给你一张图让你按顺序点中图里指定的文字或图标用来区分人和机器。能做什么拦住批量注册、刷接口的脚本。适合谁正在从 PHP 转 Golang、想用 AI 辅助写业务代码的同学。我这次的目标很明确在 ai-go-mall 项目里把点选验证码的最小闭环跑通顺便把随机数生成和文件系统操作抽成公共包为后面复用做准备。PHP 里做验证码很多人习惯直接imagecreate加imagestring几行就出图但 Golang 的image标准库更偏底层画字要自己加载字体、算坐标、处理颜色。更麻烦的是验证码天然涉及随机数、文件读取、坐标计算如果全塞在一个文件里后面维护会很难受。所以这一期的核心不是把验证码写出来而是用合理的包结构把它写出来。我试过让 AI 一次性生成整个模块结果它把 AES-256 加解密、定期清理、资源预缓存全塞进来了代码 600 多行看着就头大。后来我改成先写细需求、再让 AI 生成、最后人工 review 精简才得到能用的版本。下面把目录结构、随机数包、文件系统包、验证码生成与校验的完整流程拆开讲每一步都能跟着做。2. TaoToken 统一 Key 打通 AI 辅助编码链路转型过程中我用 AI 工具辅助写 Golang最大的痛点是多个模型、多个 Key 管理混乱。Claude Code、Codex、DeepSeek 各一套凭证切换起来很烦。TaoToken 提供统一 Key 和 API 通道把模型调用收敛到一个入口配置一次就能在多个工具里复用。先说清楚它是什么TaoToken 是一个统一的大模型 API 接入服务你拿到一个 Key就能通过兼容 OpenAI 协议的接口调用不同模型。对转型期的程序员来说好处是不用为每个工具单独申请和轮换 KeyBase URL 和 Key 配好模型 ID 按需切换即可。接入前你需要准备三样东西我称为三件套配置项值说明Base URLhttps://taotoken.net/api兼容 OpenAI 协议的接口地址API Key在控制台创建统一凭证替代多套 KeyModel ID按需选择如 claude 系列、gpt 系列等如果你用 Claude Code 这类工具配置通常写在 settings 文件里如果用 Cline 或 Codex则写在对应的 MCP 或 auth 配置中。以 Claude Code 的 settings 为例配置片段长这样{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_API_KEY: 你的统一Key } }Codex 的auth.json则是另一种写法{ base_url: https://taotoken.net/api, api_key: 你的统一Key, model: claude-sonnet-4-5 }注意Base URL 用https://taotoken.net/api不要多加路径后缀否则容易出现 404。Key 创建后只显示一次记得及时保存。模型 ID 要和你的工具支持的名称对齐写错了会报 model not found。配好之后你在终端里让 AI 帮你生成验证码代码、review 随机数包请求都会走同一条通道。这样做的好处是换模型只改 Model ID不用重新申请 Key排查问题时只看一个入口的日志团队协作时统一凭证管理。对个人转型来说省下的时间可以多写两行业务代码。需要创建 Key 的话去控制台操作https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有各工具的详细配置示例。3. 可复制配置目录结构、随机数包与文件系统包这一节给出可以直接抄的目录结构和两个公共包的完整代码。先说目录规划我采用的是分层结构基础设施放internal/infra通用工具放pkgai-go-mall/ ├── config/ │ └── captcha.yaml ├── internal/ │ ├── infra/ │ │ ├── captcha/ │ │ │ └── click.go │ │ └── config/ │ │ └── config.go │ └── model/ │ └── common.go ├── pkg/ │ ├── random/ │ │ └── random.go │ └── filesystem/ │ └── filesystem.go └── asset/ ├── captcha/ │ └── click/ │ ├── bg/ # 背景图 │ └── icon/ # 图标 └── font/ └── SourceHanSansCN-Normal.ttf配置文件config/captcha.yaml内容如下元素类型、长度、混淆数、过期时间、资源路径全放这里代码里不再硬编码captcha: elements: - chinese - uppercase - icon length: 2 confusion_length: 2 expire: 600 bg_path: asset/captcha/click/bg icon_path: asset/captcha/click/icon font_path: asset/font/SourceHanSansCN-Normal.ttf随机数包pkg/random/random.go我把它整理成两个函数加一个颜色生成器。Int用crypto/rand保证安全适合验证码这种场景FastInt用math/rand/v2适合对安全不敏感的场合package random import ( crypto/rand image/color math/big mathrand math/rand/v2 ) // Int 返回 [min, max) 范围的加密安全随机整数 func Int(min, max int) int { if max min { return min } n, err : rand.Int(rand.Reader, big.NewInt(int64(max-min))) if err ! nil { return min } return min int(n.Int64()) } // FastInt 返回 [min, max) 范围的快速随机整数 func FastInt(min, max int) int { if max min { return min } return min mathrand.IntN(max-min) } // RGB 返回随机 RGB 颜色tone 可选 dark 或 light func RGB(tone string) color.Color { if tone light { return color.RGBA{ R: uint8(180 Int(0, 75)), G: uint8(180 Int(0, 75)), B: uint8(180 Int(0, 75)), A: 255, } } return color.RGBA{ R: uint8(20 Int(0, 80)), G: uint8(20 Int(0, 80)), B: uint8(20 Int(0, 80)), A: 255, } }文件系统包pkg/filesystem/filesystem.go目前只放一个去扩展名的函数后面用到再加package filesystem import path/filepath // TrimExt 返回去除了路径和扩展名的文件名 func TrimExt(path string) string { name : filepath.Base(path) ext : filepath.Ext(name) return name[:len(name)-len(ext)] }这两个包的设计原则是只放通用逻辑不依赖 config、database 等基础设施。比如random.Int不关心验证码任何需要安全随机数的地方都能用filesystem.TrimExt不关心图标还是背景图任何路径处理都能调。迁移时我把原来click.go里的cryptoRandInt和randomTextColor搬了过来前者重写成接受min、max两个参数后者去掉random前缀直接叫RGB更通用。模型层internal/model/common.go里新增 Captcha 模型和 Token 放一起通过注册函数自动迁移func init() { Register(Token{}, Captcha{}) } type Captcha struct { Key string gorm:comment:验证码查询键;type:varchar(64);primaryKey json:key Code string gorm:comment:验证码值;type:varchar(255) json:- Info string gorm:comment:验证码数据;type:text json:- ExpiredAt time.Time gorm:comment:过期时间;not null;index json:expired_at CreatedAt time.Time gorm:comment:创建时间 json:created_at } func (Captcha) TableName() string { return captchas }注意Code和Info都加了json:-防止敏感数据泄露到 API 响应。ExpiredAt加了索引方便后续按过期时间清理。这里我取消了 AI 最初加的 AES-256 加密因为Info字段本身是明文存坐标加密Code等于加了个寂寞直接用md5(md5(code))处理一下入库就够了。4. 验证请求跑通验证码生成与校验闭环配置和包都就位后接下来验证整个链路能不能跑通。验证码模块的核心是两个方法Create生成验证码Check校验用户点击。我把它们放在internal/infra/captcha/click.go里按需初始化不在main.go里提前调用。Create的流程是这样的先随机取一张背景图然后根据配置随机生成正确元素中文、大写字母、图标再生成混淆元素打乱全部元素后绘制到背景上最后把正确元素的顺序和坐标存进数据库返回给前端的是 key、宽高、元素数组和 base64 图片。关键代码片段func Create() (*CreateResult, error) { cfg : config.Get().Captcha cleanExpired() bg, err : pickRandomFile(cfg.BgPath) if err ! nil { return nil, err } img, err : loadImage(bg) if err ! nil { return nil, err } elements : generateElements(cfg) drawElements(img, elements) key : randomKey() info, _ : json.Marshal(elements) code : md5Hex(md5Hex(joinText(elements))) record : model.Captcha{ Key: key, Code: code, Info: string(info), ExpiredAt: time.Now().Add(time.Duration(cfg.Expire) * time.Second), } if err : gorm.G[model.Captcha](db).Create(ctx, record); err ! nil { return nil, err } return CreateResult{ Key: key, Width: img.Bounds().Dx(), Height: img.Bounds().Dy(), Elements: elementTexts(elements), ImageBase64: encodeBase64(img), }, nil }Check的流程是按 key 查记录检查是否过期解密或比对正确元素序列验证用户点击顺序做坐标缩放适配前端渲染尺寸用欧几里得距离做容差校验最后按需删除记录。核心逻辑func Check(key string, clicks []ClickPoint, renderW, renderH int, deleteAfter bool) (bool, error) { record, err : gorm.G[model.Captcha](db). Where(key ?, key).First(ctx) if err ! nil { return false, err } if time.Now().After(record.ExpiredAt) { return false, nil } var elements []Element json.Unmarshal([]byte(record.Info), elements) if len(clicks) ! len(elements) { return false, nil } scaleX : float64(renderW) / float64(record.Width) scaleY : float64(renderH) / float64(record.Height) for i, el : range elements { dx : float64(clicks[i].X) - float64(el.X)*scaleX dy : float64(clicks[i].Y) - float64(el.Y)*scaleY if math.Sqrt(dx*dxdy*dy) tolerance { return false, nil } } if deleteAfter { gorm.G[model.Captcha](db).Where(key ?, key).Delete(ctx) } return true, nil }验证时我用 curl 发一个生成请求返回的 JSON 里能看到 key、宽高、元素数组和 base64 图片。把 base64 解码成图片肉眼确认元素绘制位置正确、没有重叠。然后模拟点击坐标调Check返回 true 说明校验通过。这里有个细节前端渲染尺寸可能和原图不一致所以Check里做了缩放用renderW/renderH除以原图宽高得到比例再算距离。如果你用 TaoToken 的模型对话功能让 AI 帮你 review 这段代码可以把click.go贴进去让它检查坐标缩放和容差逻辑。模型对话入口https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentchatutm_campaignrewrite 。长期做编码和 Agent 任务的话Coding Plan 更划算https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。5. 本篇常见错排查401、坐标偏移与元素重叠跑验证码的过程中我踩了几个坑这里对照真实报错说清楚怎么排查。第一个是 401 未授权。如果你在调用 AI 辅助生成代码时遇到401 Unauthorized先检查三件套是否齐全Base URL 是不是https://taotoken.net/apiKey 有没有复制完整前后不能有空格Model ID 是否拼写正确。常见错误是把 Base URL 写成带/v1的路径或者 Key 用了别的平台的。排查方法是用 curl 直接测curl https://taotoken.net/api/v1/models \ -H Authorization: Bearer 你的Key返回模型列表说明 Key 和 Base URL 没问题报 401 就是 Key 错了。第二个是坐标偏移。用户明明点对了位置Check却返回 false。原因通常是前端渲染尺寸和原图不一致但Check里没做缩放或者缩放比例算反了。排查时打印renderW、renderH、record.Width、record.Height四个值确认scaleX renderW / record.Width方向正确。另外容差tolerance设太小也会误判我一般设 15 到 20 像素。第三个是元素重叠。AI 初版没做碰撞检测极端情况下两个元素画在同一区域用户根本没法点。我在 review 时加了碰撞检测每生成一个新元素就和已生成的元素比距离小于阈值就重新生成。代码片段func hasCollision(newEl Element, existing []Element, minDist int) bool { for _, el : range existing { dx : newEl.X - el.X dy : newEl.Y - el.Y if math.Sqrt(float64(dx*dxdy*dy)) float64(minDist) { return true } } return false }第四个是reading choices类报错。如果你用某些 AI 工具时看到类似error reading choices或local proxy failed通常是工具的网络配置或代理设置有问题检查工具的 Base URL 是否指向了正确的接口地址以及本地是否有残留的代理环境变量干扰。清掉HTTP_PROXY、HTTPS_PROXY再试。第五个是 OAuth 相关报错。Claude Code 这类工具如果用 OAuth 登录配置统一 Key 时可能冲突。解决办法是在 settings 里显式指定ANTHROPIC_API_KEY并确保没有同时启用 OAuth 登录态。如果报OAuth token expired重新走一遍 Key 配置流程即可。排查顺序建议先确认三件套配置正确再用 curl 测接口通不通然后看业务代码的日志最后才怀疑 AI 生成的逻辑。大部分问题出在配置层不在代码层。6. 继续用统一 Key 推进 ai-go-mall验证码初版跑通后下一步是把它接到登录接口上替换掉原来的图形验证码。接入时注意Check的deleteAfter参数登录场景传 true验证成功立即删除防止重复提交预检场景传 false只判断对错不删记录。随机数包和文件系统包已经抽出来了后面做短信验证码、邮箱验证码时可以直接复用random.Int生成数字码用filesystem.TrimExt处理模板文件名。这就是提前封装的价值验证码只是第一个使用者不是唯一使用者。如果你也在做 PHP 转 Golang 的转型建议把 AI 当结对伙伴而不是代码生成器。先自己写细需求让 AI 出初版再人工 review 精简。我这次让 AI 生成的 600 行代码精简后核心逻辑不到 300 行去掉的都是过度设计和用不上的加密。统一 Key 的好处在这里也体现出来review 代码时随时切换模型让不同的 AI 从不同角度挑毛病而不用为每个模型单独配 Key。需要创建统一 Key 的话控制台入口https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 。API Keys 管理页https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。接入文档里有各工具的完整配置示例照着改 Base URL、Key、Model ID 三件套就能跑起来。
返回列表