代码结构

相关代码
captcha/internal/captcha/generator.go
package captchaimport (_ "embed" "image""image/color""image/draw""image/png""io""math/rand""golang.org/x/image/font""golang.org/x/image/font/opentype""golang.org/x/image/math/fixed"
)
var fontBytes []bytevar (white = color.RGBA{255, 255, 255, 255}black = color.RGBA{0, 0, 0, 255}
)func randomColor() color.Color {return color.RGBA{R: uint8(rand.Intn(256)),G: uint8(rand.Intn(256)),B: uint8(rand.Intn(256)),A: 255,}
}
func CaptchaImage(question string, w io.Writer) error {initRand()const (width = 150height = 70)img := image.NewRGBA(image.Rect(0, 0, width, height))draw.Draw(img, img.Bounds(), &image.Uniform{white}, image.Point{}, draw.Src)for i := 0; i < 5; i++ {drawLine(img, rand.Intn(width), rand.Intn(height), rand.Intn(width), rand.Intn(height), randomColor())}for i := 0; i < 50; i++ {img.Set(rand.Intn(width), rand.Intn(height), black)}if err := drawTextWithFreetype(img, question, 10, 60, randomColor()); err != nil {return err}return png.Encode(w, img)
}func drawLine(img *image.RGBA, x1, y1, x2, y2 int, c color.Color) {dx := abs(x2 - x1)dy := abs(y2 - y1)var sx, sy intif x1 < x2 {sx = 1} else {sx = -1}if y1 < y2 {sy = 1} else {sy = -1}err := dx - dyfor {img.Set(x1, y1, c)if x1 == x2 && y1 == y2 {break}e2 := 2 * errif e2 > -dy {err -= dyx1 += sx}if e2 < dx {err += dxy1 += sy}}
}func drawTextWithFreetype(img *image.RGBA, text string, x, y int, _ color.Color) error {fontParsed, err := opentype.Parse(fontBytes)if err != nil {return err}face, err := opentype.NewFace(fontParsed, &opentype.FaceOptions{Size: 32,DPI: 72,Hinting: font.HintingNone,})if err != nil {return err}currentX := xfor _, char := range text {charColor := randomColor()d := &font.Drawer{Dst: img,Src: image.NewUniform(charColor), Face: face,Dot: fixed.P(currentX, y),}d.DrawString(string(char))bounds, _ := font.BoundString(face, string(char))advance := (bounds.Max.X - bounds.Min.X).Ceil()currentX += advance + 2 }return nil
}func abs(x int) int {if x < 0 {return -x}return x
}
internal/captcha/logic.go
package captchaimport ("math/rand""strconv""strings"
)type CaptchaResult struct {Question stringAnswer intToken string
}func GenerateCaptcha() *CaptchaResult {initRand()a := rand.Intn(21) b := rand.Intn(21)var op stringvar result intif rand.Intn(2) == 0 {op = "+"result = a + b} else {op = "-"if a < b {a, b = b, a}result = a - b}question := strconv.Itoa(a) + " " + op + " " + strconv.Itoa(b) + " = ?"return &CaptchaResult{Question: question,Answer: result,Token: randString(32),}
}func randString(n int) string {const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"b := make([]byte, n)for i := range b {b[i] = letters[rand.Intn(len(letters))]}return string(b)
}func ValidateAnswer(token string, userInput string, getAnswer func(string) (string, error)) bool {userInput = strings.TrimSpace(userInput)ansStr, err := getAnswer(token)if err != nil {return false}expected, err := strconv.Atoi(ansStr)if err != nil {return false}given, err := strconv.Atoi(userInput)return err == nil && given == expected
}
internal/captcha/rand.go
package captchaimport ("math/rand""sync""time"
)var (randInit sync.Once
)func initRand() {randInit.Do(func() {rand.Seed(time.Now().UnixNano())})
}
pkg/redis/redis.go
package redisimport ("context""time""github.com/go-redis/redis/v8"
)type Client struct {*redis.Client
}func NewClient(addr, password string, db int) *Client {rdb := redis.NewClient(&redis.Options{Addr: addr,Password: password,DB: db,})return &Client{rdb}
}func (c *Client) SetCaptcha(ctx context.Context, key string, answer int, expiration time.Duration) error {return c.Set(ctx, key, answer, expiration).Err()
}func (c *Client) GetCaptcha(ctx context.Context, key string) (string, error) {return c.Get(ctx, key).Result()
}func (c *Client) DeleteCaptcha(ctx context.Context, key string) error {return c.Del(ctx, key).Err()
}
main.go
package mainimport ("context""fmt""go_collect/captcha/internal/captcha""go_collect/captcha/pkg/redis""log""net/http""time""github.com/gin-gonic/gin"
)var redisClient *redis.Clientfunc init() {redisClient = redis.NewClient("localhost:6377", "", 0)if err := redisClient.Ping(context.Background()).Err(); err != nil {log.Fatal("❌ Redis 连接失败:", err)}fmt.Println("✅ Redis 连接成功")
}func main() {r := gin.Default()r.GET("/captcha", getCaptchaHandler)r.POST("/captcha/verify", verifyCaptchaHandler)r.Run(":8088")
}func getCaptchaHandler(c *gin.Context) {cap := captcha.GenerateCaptcha()ctx := context.Background()err := redisClient.SetCaptcha(ctx, cap.Token, cap.Answer, 5*time.Minute)if err != nil {c.JSON(http.StatusInternalServerError, gin.H{"error": "生成验证码失败"})return}c.Header("Content-Type", "image/png")c.Header("X-Captcha-Token", cap.Token) err = captcha.CaptchaImage(cap.Question, c.Writer)if err != nil {c.JSON(http.StatusInternalServerError, gin.H{"error": "渲染图片失败"})return}
}type VerifyRequest struct {Token string `json:"token" binding:"required"`Answer string `json:"answer" binding:"required"`
}func verifyCaptchaHandler(c *gin.Context) {var req VerifyRequestif err := c.ShouldBindJSON(&req); err != nil {c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})return}isValid := captcha.ValidateAnswer(req.Token, req.Answer, func(token string) (string, error) {ans, err := redisClient.GetCaptcha(context.Background(), token)if err != nil {return "", err}redisClient.DeleteCaptcha(context.Background(), token)return ans, nil})if isValid {c.JSON(http.StatusOK, gin.H{"success": true,"message": "验证通过",})} else {c.JSON(http.StatusOK, gin.H{"success": false,"message": "验证失败",})}
}
调用
http://localhost:8088/captcha

验证
http://localhost:8088/captcha/verify
{"token":"sWmHAreIaA5jC7WqshKHXOjDMTH4I9kV","answer":"7"
}
{"message": "验证通过","success": true
}
