- Updated main.go to initialize email service and include it in the dependency injection container. - Refactored handlers to utilize context in service method calls, improving consistency and error handling. - Introduced new service options for upload, security, and captcha services, enhancing modularity and testability. - Removed unused repository implementations to streamline the codebase. This commit continues the effort to improve the architecture by ensuring all services are properly injected and utilized across the application.
195 lines
4.8 KiB
Go
195 lines
4.8 KiB
Go
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
|
||
cfg *config.Config
|
||
)
|
||
|
||
// 常量定义(业务相关配置,与Redis连接配置分离)
|
||
const (
|
||
redisKeyPrefix = "captcha:" // Redis键前缀(便于区分业务)
|
||
paddingValue = 3 // 验证允许的误差像素(±3px)
|
||
)
|
||
|
||
// Init 验证码图初始化
|
||
func init() {
|
||
cfg, _ = config.Load()
|
||
// 从默认仓库中获取主图
|
||
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
|
||
}
|