package captcha
import(_"embed"// 👈 启用 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")// 👇 嵌入字体文件////go:embed font/font.ttfvar fontBytes []bytevar(
white = color.RGBA{255,255,255,255}
black = color.RGBA{0,0,0,255})funcrandomColor() color.Color {return color.RGBA{
R:uint8(rand.Intn(256)),
G:uint8(rand.Intn(256)),
B:uint8(rand.Intn(256)),
A:255,}}// CaptchaImage 生成验证码图片并写入 io.WriterfuncCaptchaImage(question string, w io.Writer)error{initRand()const(
width =150
height =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)}// 画文字(使用 freetype 渲染)if err :=drawTextWithFreetype(img, question,10,60,randomColor()); err !=nil{return err
}return png.Encode(w, img)}funcdrawLine(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 - dy
for{
img.Set(x1, y1, c)if x1 == x2 && y1 == y2 {break}
e2 :=2* err
if e2 >-dy {
err -= dy
x1 += sx
}if e2 < dx {
err += dx
y1 += sy
}}}funcdrawTextWithFreetype(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 := x
for_, 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))// 手动计算字符宽度(简单估算,或使用 font.Measure)
bounds,_:= font.BoundString(face,string(char))
advance :=(bounds.Max.X - bounds.Min.X).Ceil()
currentX += advance +2// +2 为字符间距微调}returnnil}funcabs(x int)int{if x <0{return-x
}return x
}
internal/captcha/logic.go
// internal/captcha/logic.gopackage captcha
import("math/rand""strconv""strings")type CaptchaResult struct{
Question string
Answer int
Token string}funcGenerateCaptcha()*CaptchaResult {initRand()
a := rand.Intn(21)// 0-20
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),}}funcrandString(n int)string{const letters ="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
b :=make([]byte, n)for i :=range b {
b[i]= letters[rand.Intn(len(letters))]}returnstring(b)}funcValidateAnswer(token string, userInput string, getAnswer func(string)(string,error))bool{
userInput = strings.TrimSpace(userInput)
ansStr, err :=getAnswer(token)if err !=nil{returnfalse}
expected, err := strconv.Atoi(ansStr)if err !=nil{returnfalse}
given, err := strconv.Atoi(userInput)return err ==nil&& given == expected
}