feat: backend core - auth, user, role, permission, device, webhook, monitoring, cache, repository, service, middleware, API handlers

This commit is contained in:
2026-04-02 11:19:50 +08:00
parent e59a77bc49
commit dcc1f186f8
298 changed files with 62603 additions and 0 deletions

343
internal/service/captcha.go Normal file
View File

@@ -0,0 +1,343 @@
package service
import (
"bytes"
"context"
crand "crypto/rand"
"encoding/hex"
"errors"
"fmt"
"image"
"image/color"
"image/draw"
"image/png"
"math/big"
"math/rand"
"strings"
"time"
"github.com/user-management-system/internal/cache"
)
const (
captchaWidth = 120
captchaHeight = 40
captchaLength = 4 // 验证码位数
captchaTTL = 5 * time.Minute
)
// captchaChars 验证码字符集(去掉容易混淆的字符 0/O/1/I/l
const captchaChars = "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz"
// CaptchaService 图形验证码服务
type CaptchaService struct {
cache *cache.CacheManager
}
// NewCaptchaService 创建验证码服务
func NewCaptchaService(cache *cache.CacheManager) *CaptchaService {
return &CaptchaService{cache: cache}
}
// CaptchaResult 验证码生成结果
type CaptchaResult struct {
CaptchaID string // 验证码IDUUID
ImageData []byte // PNG图片字节
}
// Generate 生成图形验证码
func (s *CaptchaService) Generate(ctx context.Context) (*CaptchaResult, error) {
// 生成随机验证码文字
text, err := s.randomText(captchaLength)
if err != nil {
return nil, fmt.Errorf("生成验证码文本失败: %w", err)
}
// 生成验证码ID
captchaID, err := s.generateID()
if err != nil {
return nil, fmt.Errorf("生成验证码ID失败: %w", err)
}
// 生成图片
imgData, err := s.renderImage(text)
if err != nil {
return nil, fmt.Errorf("生成验证码图片失败: %w", err)
}
// 存入缓存(不区分大小写,存小写)
cacheKey := "captcha:" + captchaID
s.cache.Set(ctx, cacheKey, strings.ToLower(text), captchaTTL, captchaTTL)
return &CaptchaResult{
CaptchaID: captchaID,
ImageData: imgData,
}, nil
}
// Verify 验证验证码(验证后立即删除,防止重放)
func (s *CaptchaService) Verify(ctx context.Context, captchaID, answer string) bool {
if captchaID == "" || answer == "" {
return false
}
cacheKey := "captcha:" + captchaID
val, ok := s.cache.Get(ctx, cacheKey)
if !ok {
return false
}
// 删除验证码(一次性使用)
s.cache.Delete(ctx, cacheKey)
expected, ok := val.(string)
if !ok {
return false
}
return strings.ToLower(answer) == expected
}
// VerifyWithoutDelete 验证验证码但不删除(用于测试)
func (s *CaptchaService) VerifyWithoutDelete(ctx context.Context, captchaID, answer string) bool {
if captchaID == "" || answer == "" {
return false
}
cacheKey := "captcha:" + captchaID
val, ok := s.cache.Get(ctx, cacheKey)
if !ok {
return false
}
expected, ok := val.(string)
if !ok {
return false
}
return strings.ToLower(answer) == expected
}
// ValidateCaptcha 验证验证码(对外暴露,验证后删除)
func (s *CaptchaService) ValidateCaptcha(ctx context.Context, captchaID, answer string) error {
if captchaID == "" {
return errors.New("验证码ID不能为空")
}
if answer == "" {
return errors.New("验证码答案不能为空")
}
if !s.Verify(ctx, captchaID, answer) {
return errors.New("验证码错误或已过期")
}
return nil
}
// randomText 生成随机验证码文字
func (s *CaptchaService) randomText(length int) (string, error) {
chars := []byte(captchaChars)
result := make([]byte, length)
for i := range result {
n, err := crand.Int(crand.Reader, big.NewInt(int64(len(chars))))
if err != nil {
return "", err
}
result[i] = chars[n.Int64()]
}
return string(result), nil
}
// generateID 生成验证码IDcrypto/rand 保证全局唯一,无碰撞)
func (s *CaptchaService) generateID() (string, error) {
b := make([]byte, 16)
if _, err := crand.Read(b); err != nil {
return "", err
}
return fmt.Sprintf("%d-%s", time.Now().UnixNano(), hex.EncodeToString(b)), nil
}
// renderImage 将文字渲染为PNG验证码图片纯Go实现无外部字体依赖
func (s *CaptchaService) renderImage(text string) ([]byte, error) {
// 创建 RGBA 图像
img := image.NewRGBA(image.Rect(0, 0, captchaWidth, captchaHeight))
// 随机背景色(浅色)
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
bgColor := color.RGBA{
R: uint8(220 + rng.Intn(35)),
G: uint8(220 + rng.Intn(35)),
B: uint8(220 + rng.Intn(35)),
A: 255,
}
draw.Draw(img, img.Bounds(), &image.Uniform{bgColor}, image.Point{}, draw.Src)
// 绘制干扰线
for i := 0; i < 5; i++ {
lineColor := color.RGBA{
R: uint8(rng.Intn(200)),
G: uint8(rng.Intn(200)),
B: uint8(rng.Intn(200)),
A: 255,
}
x1 := rng.Intn(captchaWidth)
y1 := rng.Intn(captchaHeight)
x2 := rng.Intn(captchaWidth)
y2 := rng.Intn(captchaHeight)
drawLine(img, x1, y1, x2, y2, lineColor)
}
// 绘制文字(使用像素字体)
for i, ch := range text {
charColor := color.RGBA{
R: uint8(rng.Intn(150)),
G: uint8(rng.Intn(150)),
B: uint8(rng.Intn(150)),
A: 255,
}
x := 10 + i*25 + rng.Intn(5)
y := 8 + rng.Intn(12)
drawChar(img, x, y, byte(ch), charColor)
}
// 绘制干扰点
for i := 0; i < 80; i++ {
dotColor := color.RGBA{
R: uint8(rng.Intn(255)),
G: uint8(rng.Intn(255)),
B: uint8(rng.Intn(255)),
A: uint8(100 + rng.Intn(100)),
}
img.Set(rng.Intn(captchaWidth), rng.Intn(captchaHeight), dotColor)
}
// 编码为 PNG
var buf bytes.Buffer
if err := png.Encode(&buf, img); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// drawLine 画直线Bresenham算法
func drawLine(img *image.RGBA, x1, y1, x2, y2 int, c color.RGBA) {
dx := abs(x2 - x1)
dy := abs(y2 - y1)
sx, sy := 1, 1
if x1 > x2 {
sx = -1
}
if y1 > y2 {
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
}
}
}
func abs(x int) int {
if x < 0 {
return -x
}
return x
}
// pixelFont 5x7 像素字体位图ASCII 32-127
// 每个字符用5个uint8表示5列每个uint8的低7位是每行是否亮起
var pixelFont = map[byte][5]uint8{
'0': {0x3E, 0x51, 0x49, 0x45, 0x3E},
'1': {0x00, 0x42, 0x7F, 0x40, 0x00},
'2': {0x42, 0x61, 0x51, 0x49, 0x46},
'3': {0x21, 0x41, 0x45, 0x4B, 0x31},
'4': {0x18, 0x14, 0x12, 0x7F, 0x10},
'5': {0x27, 0x45, 0x45, 0x45, 0x39},
'6': {0x3C, 0x4A, 0x49, 0x49, 0x30},
'7': {0x01, 0x71, 0x09, 0x05, 0x03},
'8': {0x36, 0x49, 0x49, 0x49, 0x36},
'9': {0x06, 0x49, 0x49, 0x29, 0x1E},
'A': {0x7E, 0x11, 0x11, 0x11, 0x7E},
'B': {0x7F, 0x49, 0x49, 0x49, 0x36},
'C': {0x3E, 0x41, 0x41, 0x41, 0x22},
'D': {0x7F, 0x41, 0x41, 0x22, 0x1C},
'E': {0x7F, 0x49, 0x49, 0x49, 0x41},
'F': {0x7F, 0x09, 0x09, 0x09, 0x01},
'G': {0x3E, 0x41, 0x49, 0x49, 0x7A},
'H': {0x7F, 0x08, 0x08, 0x08, 0x7F},
'J': {0x20, 0x40, 0x41, 0x3F, 0x01},
'K': {0x7F, 0x08, 0x14, 0x22, 0x41},
'L': {0x7F, 0x40, 0x40, 0x40, 0x40},
'M': {0x7F, 0x02, 0x0C, 0x02, 0x7F},
'N': {0x7F, 0x04, 0x08, 0x10, 0x7F},
'P': {0x7F, 0x09, 0x09, 0x09, 0x06},
'Q': {0x3E, 0x41, 0x51, 0x21, 0x5E},
'R': {0x7F, 0x09, 0x19, 0x29, 0x46},
'S': {0x46, 0x49, 0x49, 0x49, 0x31},
'T': {0x01, 0x01, 0x7F, 0x01, 0x01},
'U': {0x3F, 0x40, 0x40, 0x40, 0x3F},
'V': {0x1F, 0x20, 0x40, 0x20, 0x1F},
'W': {0x3F, 0x40, 0x38, 0x40, 0x3F},
'X': {0x63, 0x14, 0x08, 0x14, 0x63},
'Y': {0x07, 0x08, 0x70, 0x08, 0x07},
'Z': {0x61, 0x51, 0x49, 0x45, 0x43},
'a': {0x20, 0x54, 0x54, 0x54, 0x78},
'b': {0x7F, 0x48, 0x44, 0x44, 0x38},
'c': {0x38, 0x44, 0x44, 0x44, 0x20},
'd': {0x38, 0x44, 0x44, 0x48, 0x7F},
'e': {0x38, 0x54, 0x54, 0x54, 0x18},
'f': {0x08, 0x7E, 0x09, 0x01, 0x02},
'g': {0x0C, 0x52, 0x52, 0x52, 0x3E},
'h': {0x7F, 0x08, 0x04, 0x04, 0x78},
'j': {0x20, 0x40, 0x44, 0x3D, 0x00},
'k': {0x7F, 0x10, 0x28, 0x44, 0x00},
'm': {0x7C, 0x04, 0x18, 0x04, 0x78},
'n': {0x7C, 0x08, 0x04, 0x04, 0x78},
'p': {0x7C, 0x14, 0x14, 0x14, 0x08},
'q': {0x08, 0x14, 0x14, 0x18, 0x7C},
'r': {0x7C, 0x08, 0x04, 0x04, 0x08},
's': {0x48, 0x54, 0x54, 0x54, 0x20},
't': {0x04, 0x3F, 0x44, 0x40, 0x20},
'u': {0x3C, 0x40, 0x40, 0x20, 0x7C},
'v': {0x1C, 0x20, 0x40, 0x20, 0x1C},
'w': {0x3C, 0x40, 0x30, 0x40, 0x3C},
'x': {0x44, 0x28, 0x10, 0x28, 0x44},
'y': {0x0C, 0x50, 0x50, 0x50, 0x3C},
'z': {0x44, 0x64, 0x54, 0x4C, 0x44},
}
// drawChar 在图像上绘制单个字符
func drawChar(img *image.RGBA, x, y int, ch byte, c color.RGBA) {
glyph, ok := pixelFont[ch]
if !ok {
// 未知字符画个方块
for dy := 0; dy < 7; dy++ {
for dx := 0; dx < 5; dx++ {
img.Set(x+dx*2, y+dy*2, c)
}
}
return
}
for col, colData := range glyph {
for row := 0; row < 7; row++ {
if colData&(1<<uint(row)) != 0 {
// 放大2倍绘制
img.Set(x+col*2, y+row*2, c)
img.Set(x+col*2+1, y+row*2, c)
img.Set(x+col*2, y+row*2+1, c)
img.Set(x+col*2+1, y+row*2+1, c)
}
}
}
}