package service import ( "image" "image/color" "image/draw" "image/png" "math/rand" "bytes" "encoding/base64" "fmt" "sync" "time" "golang.org/x/image/font" "golang.org/x/image/font/basicfont" "golang.org/x/image/math/fixed" ) // CaptchaItem 表示一个验证码条目 type CaptchaItem struct { ID string Answer string ExpiresAt time.Time } var ( captchaStore = sync.Map{} // map[string]*CaptchaItem captchaCleaner sync.Once ) func initCaptchaCleaner() { go func() { for { time.Sleep(5 * time.Minute) now := time.Now() captchaStore.Range(func(key, value interface{}) bool { item := value.(*CaptchaItem) if now.After(item.ExpiresAt) { captchaStore.Delete(key) } return true }) } }() } // GenerateMathCaptcha 生成一个数学验证码,返回 id、问题表达式、base64 PNG 图片 func GenerateMathCaptcha() (id string, question string, base64PNG string, err error) { captchaCleaner.Do(initCaptchaCleaner) // 生成随机数学题 a := rand.Intn(20) + 1 // 1-20 b := rand.Intn(20) + 1 // 1-20 op := rand.Intn(2) // 0: +, 1: - var answer int var opStr string if op == 0 { answer = a + b opStr = "+" } else { // 确保结果非负 if a < b { a, b = b, a } answer = a - b opStr = "-" } question = fmt.Sprintf("%d %s %d = ?", a, opStr, b) answerStr := fmt.Sprintf("%d", answer) img, err := drawCaptchaImage(question) if err != nil { return "", "", "", err } var buf bytes.Buffer if err := png.Encode(&buf, img); err != nil { return "", "", "", err } base64PNG = "data:image/png;base64," + base64.StdEncoding.EncodeToString(buf.Bytes()) // 生成唯一 ID 并存储 id = fmt.Sprintf("%d%06d", time.Now().UnixNano(), rand.Intn(1000000)) captchaStore.Store(id, &CaptchaItem{ ID: id, Answer: answerStr, ExpiresAt: time.Now().Add(5 * time.Minute), }) return id, question, base64PNG, nil } // VerifyCaptcha 验证验证码,验证后立即使其失效 func VerifyCaptcha(id string, answer string) bool { if id == "" || answer == "" { return false } val, ok := captchaStore.LoadAndDelete(id) if !ok { return false } item := val.(*CaptchaItem) if time.Now().After(item.ExpiresAt) { return false } return item.Answer == answer } // drawCaptchaImage 将验证码文字绘制为带噪点的图片 func drawCaptchaImage(text string) (*image.RGBA, error) { width := 160 height := 50 img := image.NewRGBA(image.Rect(0, 0, width, height)) // 随机浅色背景 bgR := uint8(220 + rand.Intn(36)) bgG := uint8(220 + rand.Intn(36)) bgB := uint8(220 + rand.Intn(36)) bgColor := color.RGBA{bgR, bgG, bgB, 255} draw.Draw(img, img.Bounds(), &image.Uniform{bgColor}, image.Point{}, draw.Src) // 绘制干扰线 for i := 0; i < 4; i++ { x1 := rand.Intn(width) y1 := rand.Intn(height) x2 := rand.Intn(width) y2 := rand.Intn(height) lineColor := color.RGBA{ uint8(rand.Intn(150)), uint8(rand.Intn(150)), uint8(rand.Intn(150)), 255, } drawLine(img, x1, y1, x2, y2, lineColor) } // 绘制干扰点 for i := 0; i < 60; i++ { x := rand.Intn(width) y := rand.Intn(height) dotColor := color.RGBA{ uint8(rand.Intn(200)), uint8(rand.Intn(200)), uint8(rand.Intn(200)), 255, } img.Set(x, y, dotColor) } // 绘制文字 face := basicfont.Face7x13 d := &font.Drawer{ Dst: img, Src: &image.Uniform{color.RGBA{50, 50, 50, 255}}, Face: face, } // 计算文字宽度以居中 textWidth := font.MeasureString(face, text).Ceil() startX := (width - textWidth) / 2 if startX < 5 { startX = 5 } d.Dot = fixed.P(startX, 30) d.DrawString(text) return img, nil } // drawLine 在图像上绘制一条线段(Bresenham 算法简化版) func drawLine(img *image.RGBA, x1, y1, x2, y2 int, c color.RGBA) { dx := abs(x2 - x1) dy := abs(y2 - y1) steps := dx if dy > dx { steps = dy } if steps == 0 { img.Set(x1, y1, c) return } for i := 0; i <= steps; i++ { x := x1 + (x2-x1)*i/steps y := y1 + (y2-y1)*i/steps // 线宽为 2 img.Set(x, y, c) if x+1 < img.Bounds().Max.X { img.Set(x+1, y, c) } } } func abs(x int) int { if x < 0 { return -x } return x }