Files
cpdaily_xiaozhushou/backed/internal/service/captcha.go
T
becomer 2e2c161770 feat: init cpdaily-xiaozhushou with blue-green deploy
- backed/: Go Fiber backend with cpdaily integration
- frondend/: Vue 3 + Vite frontend
- Dockerfile: multi-stage build (Node → Go → Alpine)
- deploy/docker-compose.yml: production compose file
- .woodpecker/build.yml: CI/CD with blue-green zero-downtime deploy
- /health endpoint for deployment health checks
- Port 8884/8885 blue-green swap via 1Panel OpenResty
2026-07-03 15:37:55 +08:00

446 lines
10 KiB
Go

package service
import (
"bytes"
"cpdaily-new/internal/config"
"encoding/base64"
"image"
"image/color"
"image/draw"
"image/png"
"math/rand"
"strings"
"sync"
"time"
)
var (
captchaStore = sync.Map{}
rng = rand.New(rand.NewSource(time.Now().UnixNano()))
)
type CaptchaResult struct {
CaptchaID string `json:"captchaId"`
ImageBase64 string `json:"imageBase64"`
}
// 二次元风格配色 — 亮色系
var animeColors = []color.RGBA{
{0xFF, 0x14, 0x93, 0xFF}, // 深粉红
{0x00, 0xCE, 0xD1, 0xFF}, // 青色
{0xFF, 0xD7, 0x00, 0xFF}, // 金色
{0x93, 0x70, 0xDB, 0xFF}, // 中紫
{0xFF, 0x8C, 0x00, 0xFF}, // 橙色
{0x00, 0xBF, 0xFF, 0xFF}, // 天蓝
{0xBA, 0x55, 0xD3, 0xFF}, // 紫红
{0x32, 0xCD, 0x32, 0xFF}, // 酸橙
{0xFF, 0x69, 0xB4, 0xFF}, // 亮粉
{0xFF, 0x63, 0x47, 0xFF}, // 番茄
}
// 马卡龙色系背景
var pastelBG = []color.RGBA{
{0xFF, 0xE4, 0xE1, 0xFF}, // 淡粉
{0xE6, 0xE6, 0xFA, 0xFF}, // 淡紫
{0xF0, 0xFF, 0xF0, 0xFF}, // 薄荷
{0xFF, 0xDA, 0xB9, 0xFF}, // 桃色
{0xE0, 0xF0, 0xFF, 0xFF}, // 天蓝
{0xFF, 0xF0, 0xF5, 0xFF}, // 薰衣草
{0xF5, 0xFF, 0xFA, 0xFF}, // 薄荷奶油
{0xFF, 0xF8, 0xDC, 0xFF}, // 玉米丝
}
// 生成随机验证码
func generateCode(length int) string {
digits := "0123456789"
var sb strings.Builder
for i := 0; i < length; i++ {
sb.WriteByte(digits[rng.Intn(len(digits))])
}
return sb.String()
}
// 生成图片验证码
func GenerateCaptcha() (*CaptchaResult, error) {
cfg := config.Global.Captcha
code := generateCode(cfg.Length)
// 生成图片
img := image.NewRGBA(image.Rect(0, 0, cfg.Width, cfg.Height))
// 随机马卡龙色背景
bg := pastelBG[rng.Intn(len(pastelBG))]
draw.Draw(img, img.Bounds(), &image.Uniform{bg}, image.Point{}, draw.Src)
// ── 干扰线(粗线,高不透明度)──
for i := 0; i < 10; i++ {
col := animeColors[rng.Intn(len(animeColors))]
col.A = uint8(rng.Intn(80) + 150) // alpha 150~230
x1 := rng.Intn(cfg.Width)
y1 := rng.Intn(cfg.Height)
x2 := rng.Intn(cfg.Width)
y2 := rng.Intn(cfg.Height)
thick := rng.Intn(2) + 2 // 2~3px 粗
drawThickLine(img, x1, y1, x2, y2, col, thick)
}
// ── 二次元风格彩色折线 ──
for i := 0; i < 6; i++ {
col := animeColors[rng.Intn(len(animeColors))]
col.A = uint8(rng.Intn(60) + 130)
x1 := rng.Intn(cfg.Width)
y1 := rng.Intn(cfg.Height)
xm := rng.Intn(cfg.Width)
ym := rng.Intn(cfg.Height)
x2 := rng.Intn(cfg.Width)
y2 := rng.Intn(cfg.Height)
drawThickLine(img, x1, y1, xm, ym, col, 2)
drawThickLine(img, xm, ym, x2, y2, col, 2)
}
// ── 装饰圆圈(半透明彩色气泡)──
for i := 0; i < 4; i++ {
cx := rng.Intn(cfg.Width)
cy := rng.Intn(cfg.Height)
radius := rng.Intn(5) + 3
col := animeColors[rng.Intn(len(animeColors))]
col.A = 40
drawCircle(img, cx, cy, radius, col)
}
// 小星星/闪光装饰点
for i := 0; i < 6; i++ {
x := rng.Intn(cfg.Width)
y := rng.Intn(cfg.Height)
starCol := animeColors[rng.Intn(len(animeColors))]
starCol.A = 100
drawStarDot(img, x, y, starCol)
}
// 彩色噪点
for i := 0; i < 100; i++ {
x := rng.Intn(cfg.Width)
y := rng.Intn(cfg.Height)
col := animeColors[rng.Intn(len(animeColors))]
col.A = uint8(rng.Intn(100) + 80)
img.Set(x, y, col)
}
// ── 绘制文字 — 4px 像素块,带倾斜效果 ──
charBlockW := cfg.Width / cfg.Length
charPixelSize := 4 // 4px 像素块
charMapW := 7 // 字符矩阵宽度
charMapH := 9 // 字符矩阵高度
charW := charMapW * charPixelSize
charH := charMapH * charPixelSize
for i, ch := range code {
// 每个字符在其块中居中
blockCenterX := i*charBlockW + charBlockW/2
charX := blockCenterX - charW/2
charY := (cfg.Height - charH) / 2
// 微调位置
charX += rng.Intn(5) - 2
charY += rng.Intn(3) - 1
// 每个字符选择鲜艳的二次元颜色
charColor := animeColors[rng.Intn(len(animeColors))]
// 随机倾斜程度 (shear: 每行偏移像素数)
shear := rng.Intn(2) + 1 // 1~2
// 1/3 概率向右倾斜,2/3 向左倾斜
if rng.Intn(3) != 0 {
shear = -shear
}
drawCharLarge(img, charX, charY, string(ch), charColor, charPixelSize, shear)
}
// 编码为 PNG base64
var buf bytes.Buffer
if err := png.Encode(&buf, img); err != nil {
return nil, err
}
base64Str := base64.StdEncoding.EncodeToString(buf.Bytes())
// 生成唯一 ID 并存储验证码
captchaID := generateCode(16)
captchaStore.Store(captchaID, CaptchaValue{
Code: code,
ExpiresAt: time.Now().Add(5 * time.Minute),
})
return &CaptchaResult{
CaptchaID: captchaID,
ImageBase64: "data:image/png;base64," + base64Str,
}, nil
}
type CaptchaValue struct {
Code string
ExpiresAt time.Time
}
func VerifyCaptcha(captchaID, code string) bool {
if captchaID == "" || code == "" {
return false
}
val, ok := captchaStore.Load(captchaID)
if !ok {
return false
}
captchaStore.Delete(captchaID)
cv := val.(CaptchaValue)
if time.Now().After(cv.ExpiresAt) {
return false
}
return strings.EqualFold(cv.Code, code)
}
// GetCaptchaDebug 读取验证码(调试用)
func GetCaptchaDebug(captchaID string) (string, bool) {
val, ok := captchaStore.Load(captchaID)
if !ok {
return "", false
}
cv := val.(CaptchaValue)
return cv.Code, true
}
// 画粗线(绘制多条平行线模拟粗线)
func drawThickLine(img *image.RGBA, x1, y1, x2, y2 int, col color.Color, thickness int) {
dx, dy := x2-x1, y2-y1
steps := max(abs(dx), abs(dy))
if steps == 0 {
return
}
for i := 0; i <= steps; i++ {
cx := x1 + dx*i/steps
cy := y1 + dy*i/steps
// 在垂直方向偏移绘制粗线效果
for t := -(thickness - 1); t < thickness; t++ {
// 根据线的方向决定偏移方向
var px, py int
if abs(dx) > abs(dy) {
px = cx
py = cy + t
} else {
px = cx + t
py = cy
}
if px >= 0 && px < img.Bounds().Max.X && py >= 0 && py < img.Bounds().Max.Y {
img.Set(px, py, col)
}
}
}
}
// 划线(单像素)
func drawLine(img *image.RGBA, x1, y1, x2, y2 int, col color.Color) {
dx, dy := x2-x1, y2-y1
steps := max(abs(dx), abs(dy))
if steps == 0 {
return
}
for i := 0; i <= steps; i++ {
x := x1 + dx*i/steps
y := y1 + dy*i/steps
if x >= 0 && x < img.Bounds().Max.X && y >= 0 && y < img.Bounds().Max.Y {
img.Set(x, y, col)
}
}
}
// 绘制半透明圆圈
func drawCircle(img *image.RGBA, cx, cy, radius int, col color.Color) {
for dy := -radius; dy <= radius; dy++ {
for dx := -radius; dx <= radius; dx++ {
if dx*dx+dy*dy <= radius*radius {
x := cx + dx
y := cy + dy
if x >= 0 && x < img.Bounds().Max.X && y >= 0 && y < img.Bounds().Max.Y {
img.Set(x, y, col)
}
}
}
}
}
// 绘制小星星闪光点
func drawStarDot(img *image.RGBA, x, y int, col color.Color) {
offsets := [][2]int{
{0, 0}, {1, 0}, {-1, 0}, {0, 1}, {0, -1},
{1, 1}, {-1, -1}, {1, -1}, {-1, 1},
}
for _, off := range offsets {
px := x + off[0]
py := y + off[1]
if px >= 0 && px < img.Bounds().Max.X && py >= 0 && py < img.Bounds().Max.Y {
img.Set(px, py, col)
}
}
}
// 使用 7x9 点阵绘制大号字符(带倾斜 shear 效果)
func drawCharLarge(img *image.RGBA, x, y int, char string, col color.Color, pixelSize int, shear int) {
// 7x9 数字点阵映射
charMaps := map[string][][]int{
"0": {
{0, 1, 1, 1, 1, 1, 0},
{1, 1, 1, 1, 1, 1, 1},
{1, 1, 0, 0, 0, 1, 1},
{1, 1, 0, 0, 0, 1, 1},
{1, 1, 0, 0, 0, 1, 1},
{1, 1, 0, 0, 0, 1, 1},
{1, 1, 0, 0, 0, 1, 1},
{1, 1, 1, 1, 1, 1, 1},
{0, 1, 1, 1, 1, 1, 0},
},
"1": {
{0, 0, 0, 1, 0, 0, 0},
{0, 0, 1, 1, 0, 0, 0},
{0, 1, 1, 1, 0, 0, 0},
{0, 0, 0, 1, 0, 0, 0},
{0, 0, 0, 1, 0, 0, 0},
{0, 0, 0, 1, 0, 0, 0},
{0, 0, 0, 1, 0, 0, 0},
{0, 0, 0, 1, 0, 0, 0},
{0, 1, 1, 1, 1, 1, 0},
},
"2": {
{0, 1, 1, 1, 1, 1, 0},
{1, 1, 1, 1, 1, 1, 1},
{0, 0, 0, 0, 0, 1, 1},
{0, 0, 0, 0, 0, 1, 1},
{0, 1, 1, 1, 1, 1, 0},
{1, 1, 0, 0, 0, 0, 0},
{1, 1, 0, 0, 0, 0, 0},
{1, 1, 1, 1, 1, 1, 1},
{0, 1, 1, 1, 1, 1, 0},
},
"3": {
{0, 1, 1, 1, 1, 1, 0},
{1, 1, 1, 1, 1, 1, 1},
{0, 0, 0, 0, 0, 1, 1},
{0, 0, 0, 0, 0, 1, 1},
{0, 1, 1, 1, 1, 1, 0},
{0, 0, 0, 0, 0, 1, 1},
{0, 0, 0, 0, 0, 1, 1},
{1, 1, 1, 1, 1, 1, 1},
{0, 1, 1, 1, 1, 1, 0},
},
"4": {
{0, 0, 0, 0, 1, 1, 0},
{0, 0, 0, 1, 1, 1, 0},
{0, 0, 1, 1, 1, 1, 0},
{0, 1, 1, 0, 1, 1, 0},
{1, 1, 0, 0, 1, 1, 0},
{1, 1, 1, 1, 1, 1, 1},
{0, 0, 0, 0, 1, 1, 0},
{0, 0, 0, 0, 1, 1, 0},
{0, 0, 0, 0, 1, 1, 0},
},
"5": {
{1, 1, 1, 1, 1, 1, 1},
{1, 1, 1, 1, 1, 1, 1},
{1, 1, 0, 0, 0, 0, 0},
{1, 1, 0, 0, 0, 0, 0},
{0, 1, 1, 1, 1, 1, 0},
{0, 0, 0, 0, 0, 1, 1},
{0, 0, 0, 0, 0, 1, 1},
{1, 1, 1, 1, 1, 1, 1},
{0, 1, 1, 1, 1, 1, 0},
},
"6": {
{0, 1, 1, 1, 1, 1, 0},
{1, 1, 1, 1, 1, 1, 1},
{1, 1, 0, 0, 0, 0, 0},
{1, 1, 0, 0, 0, 0, 0},
{1, 1, 1, 1, 1, 1, 0},
{1, 1, 0, 0, 0, 1, 1},
{1, 1, 0, 0, 0, 1, 1},
{1, 1, 1, 1, 1, 1, 1},
{0, 1, 1, 1, 1, 1, 0},
},
"7": {
{1, 1, 1, 1, 1, 1, 1},
{1, 1, 1, 1, 1, 1, 1},
{0, 0, 0, 0, 0, 1, 1},
{0, 0, 0, 0, 0, 1, 1},
{0, 0, 0, 0, 1, 1, 0},
{0, 0, 0, 1, 1, 0, 0},
{0, 0, 1, 1, 0, 0, 0},
{0, 0, 1, 1, 0, 0, 0},
{0, 0, 1, 1, 0, 0, 0},
},
"8": {
{0, 1, 1, 1, 1, 1, 0},
{1, 1, 1, 1, 1, 1, 1},
{1, 1, 0, 0, 0, 1, 1},
{1, 1, 0, 0, 0, 1, 1},
{0, 1, 1, 1, 1, 1, 0},
{1, 1, 0, 0, 0, 1, 1},
{1, 1, 0, 0, 0, 1, 1},
{1, 1, 1, 1, 1, 1, 1},
{0, 1, 1, 1, 1, 1, 0},
},
"9": {
{0, 1, 1, 1, 1, 1, 0},
{1, 1, 1, 1, 1, 1, 1},
{1, 1, 0, 0, 0, 1, 1},
{1, 1, 0, 0, 0, 1, 1},
{0, 1, 1, 1, 1, 1, 1},
{0, 0, 0, 0, 0, 1, 1},
{0, 0, 0, 0, 0, 1, 1},
{1, 1, 1, 1, 1, 1, 1},
{0, 1, 1, 1, 1, 1, 0},
},
}
pattern, ok := charMaps[char]
if !ok {
return
}
// shear: 每行相对中心行(4)的水平偏移
// shear=1: 行0偏移-4, 行8偏移+4 → 右倾
// shear=-1: 行0偏移+4, 行8偏移-4 → 左倾
for py, row := range pattern {
// 行偏移量 = (py - 4) * shear
rowShear := (py - 4) * shear
for px, v := range row {
if v == 1 {
ix := x + px*pixelSize + rowShear
iy := y + py*pixelSize
for dx := 0; dx < pixelSize; dx++ {
for dy := 0; dy < pixelSize; dy++ {
if ix+dx < img.Bounds().Max.X && iy+dy < img.Bounds().Max.Y {
img.Set(ix+dx, iy+dy, col)
}
}
}
}
}
}
}
func abs(x int) int {
if x < 0 {
return -x
}
return x
}
func max(a, b int) int {
if a > b {
return a
}
return b
}