package service import ( "carrotskin/pkg/config" "carrotskin/pkg/redis" "context" "errors" "fmt" "log" "time" "github.com/google/uuid" "github.com/wenlng/go-captcha-assets/resources/imagesv2" "github.com/wenlng/go-captcha-assets/resources/tiles" "github.com/wenlng/go-captcha/v2/slide" "go.uber.org/zap" ) var ( slideTileCapt slide.Captcha ) // 常量定义(业务相关配置,与Redis连接配置分离) const ( redisKeyPrefix = "captcha:" // Redis键前缀(便于区分业务) paddingValue = 3 // 验证允许的误差像素(±3px) ) // Init 验证码图初始化 func init() { builder := slide.NewBuilder() bgImage, err := imagesv2.GetImages() if err != nil { log.Fatalln(err) } // 滑块形状获取 graphs := getSlideTileGraphArr() builder.SetResources( slide.WithGraphImages(graphs), slide.WithBackgrounds(bgImage), ) slideTileCapt = builder.Make() if slideTileCapt == nil { log.Fatalln("验证码实例初始化失败") } } // getSlideTileGraphArr 滑块选择 func getSlideTileGraphArr() []*slide.GraphImage { graphs, err := tiles.GetTiles() if err != nil { log.Fatalln(err) } var newGraphs = make([]*slide.GraphImage, 0, len(graphs)) for i := 0; i < len(graphs); i++ { graph := graphs[i] newGraphs = append(newGraphs, &slide.GraphImage{ OverlayImage: graph.OverlayImage, MaskImage: graph.MaskImage, ShadowImage: graph.ShadowImage, }) } return newGraphs } // RedisData 存储到Redis的验证信息(仅包含校验必需字段) type RedisData struct { Tx int `json:"tx"` // 滑块目标X坐标 Ty int `json:"ty"` // 滑块目标Y坐标 } // captchaService CaptchaService的实现 type captchaService struct { redis *redis.Client logger *zap.Logger } // NewCaptchaService 创建CaptchaService实例 func NewCaptchaService(redisClient *redis.Client, logger *zap.Logger) CaptchaService { return &captchaService{ redis: redisClient, logger: logger, } } // Generate 生成验证码 func (s *captchaService) Generate(ctx context.Context) (masterImg, tileImg, captchaID string, y int, err error) { // 生成uuid作为验证码进程唯一标识 captchaID = uuid.NewString() if captchaID == "" { err = errors.New("生成验证码唯一标识失败") return } captData, err := slideTileCapt.Generate() if err != nil { err = fmt.Errorf("生成验证码失败: %w", err) return } blockData := captData.GetData() if blockData == nil { err = errors.New("获取验证码数据失败") return } block, _ := json.Marshal(blockData) var blockMap map[string]interface{} if err = json.Unmarshal(block, &blockMap); err != nil { err = fmt.Errorf("反序列化为map失败: %w", err) return } // 提取x和y并转换为int类型 tx, ok := blockMap["x"].(float64) if !ok { err = errors.New("无法将x转换为float64") return } var x = int(tx) ty, ok := blockMap["y"].(float64) if !ok { err = errors.New("无法将y转换为float64") return } y = int(ty) masterImg, err = captData.GetMasterImage().ToBase64() if err != nil { err = fmt.Errorf("主图转换为base64失败: %w", err) return } tileImg, err = captData.GetTileImage().ToBase64() if err != nil { err = fmt.Errorf("滑块图转换为base64失败: %w", err) return } redisData := RedisData{ Tx: x, Ty: y, } redisDataJSON, _ := json.Marshal(redisData) redisKey := redisKeyPrefix + captchaID expireTime := 300 * time.Second // 使用注入的Redis客户端 if err = s.redis.Set(ctx, redisKey, redisDataJSON, expireTime); err != nil { err = fmt.Errorf("存储验证码到redis失败: %w", err) return } // 返回时 y 需要减10 y = y - 10 return } // Verify 验证验证码 func (s *captchaService) Verify(ctx context.Context, dx int, captchaID string) (bool, error) { // 测试环境下直接通过验证 cfg, err := config.GetConfig() if err == nil && cfg.IsTestEnvironment() { return true, nil } redisKey := redisKeyPrefix + captchaID // 从Redis获取验证信息,使用注入的客户端 dataJSON, err := s.redis.Get(ctx, redisKey) if err != nil { if s.redis.Nil(err) { // 使用封装客户端的Nil错误 return false, errors.New("验证码已过期或无效") } return false, fmt.Errorf("redis查询失败: %w", err) } var redisData RedisData if err := json.Unmarshal([]byte(dataJSON), &redisData); err != nil { return false, fmt.Errorf("解析redis数据失败: %w", err) } tx := redisData.Tx ty := redisData.Ty ok := slide.Validate(dx, ty, tx, ty, paddingValue) // 验证后立即删除Redis记录(防止重复使用) if ok { if err := s.redis.Del(ctx, redisKey); err != nil { // 记录警告但不影响验证结果 s.logger.Warn("删除验证码Redis记录失败", zap.Error(err)) } } return ok, nil }