Files
junhong_cmp_fiber/internal/service/verification/service.go
huang 409a68d60b
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m45s
feat: OpenAPI 契约对齐与框架优化
主要变更:
1. OpenAPI 文档契约对齐
   - 统一错误响应字段名为 msg(非 message)
   - 规范 envelope 响应结构(code, msg, data, timestamp)
   - 个人客户路由纳入文档体系(使用 Register 机制)
   - 新增 BuildDocHandlers() 统一管理 handler 构造
   - 确保文档生成的幂等性

2. Service 层错误处理统一
   - 全面替换 fmt.Errorf 为 errors.New/Wrap
   - 统一错误码使用规范
   - Handler 层参数校验不泄露底层细节
   - 新增错误码验证集成测试

3. 代码质量提升
   - 删除未使用的 Task handler 和路由
   - 新增代码规范检查脚本(check-service-errors.sh)
   - 新增注释路径一致性检查(check-comment-paths.sh)
   - 更新 API 文档生成指南

4. OpenSpec 归档
   - 归档 openapi-contract-alignment 变更(63 tasks)
   - 归档 service-error-unify-core 变更
   - 归档 service-error-unify-support 变更
   - 归档 code-cleanup-docs-update 变更
   - 归档 handler-validation-security 变更
   - 同步 delta specs 到主规范文件

影响范围:
- pkg/openapi: 新增 handlers.go,优化 generator.go
- internal/service/*: 48 个 service 文件错误处理统一
- internal/handler/admin: 优化参数校验错误提示
- internal/routes: 个人客户路由改造,删除 task 路由
- scripts: 新增 3 个代码检查脚本
- docs: 更新 OpenAPI 文档(15750+ 行)
- openspec/specs: 同步 3 个主规范文件

破坏性变更:无
向后兼容:是
2026-01-30 11:40:36 +08:00

182 lines
4.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package verification 提供验证码管理的业务逻辑服务
// 包含短信验证码生成、发送、验证等功能
package verification
import (
"context"
"crypto/rand"
"fmt"
"math/big"
"github.com/break/junhong_cmp_fiber/pkg/config"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"github.com/break/junhong_cmp_fiber/pkg/sms"
"github.com/redis/go-redis/v9"
"go.uber.org/zap"
)
// Service 验证码服务
type Service struct {
redisClient *redis.Client
smsClient *sms.Client
logger *zap.Logger
}
// NewService 创建验证码服务实例
func NewService(redisClient *redis.Client, smsClient *sms.Client, logger *zap.Logger) *Service {
return &Service{
redisClient: redisClient,
smsClient: smsClient,
logger: logger,
}
}
// SendCode 发送验证码
func (s *Service) SendCode(ctx context.Context, phone string) error {
// 检查短信服务是否可用
if s.smsClient == nil {
s.logger.Error("短信服务未配置", zap.String("phone", phone))
return errors.New(errors.CodeServiceUnavailable)
}
// 检查发送频率限制
limitKey := constants.RedisVerificationCodeLimitKey(phone)
exists, err := s.redisClient.Exists(ctx, limitKey).Result()
if err != nil {
s.logger.Error("检查验证码发送频率限制失败",
zap.String("phone", phone),
zap.Error(err),
)
return errors.Wrap(errors.CodeInternalError, err, "检查验证码发送频率限制失败")
}
if exists > 0 {
s.logger.Warn("验证码发送过于频繁",
zap.String("phone", phone),
)
return errors.New(errors.CodeTooManyRequests, "验证码发送过于频繁,请稍后再试")
}
// 生成随机验证码
code, err := s.generateCode()
if err != nil {
s.logger.Error("生成验证码失败",
zap.String("phone", phone),
zap.Error(err),
)
return errors.Wrap(errors.CodeInternalError, err, "生成验证码失败")
}
// 构造短信内容
cfg := config.Get()
content := fmt.Sprintf("您的验证码是%s%d分钟内有效", code, int(constants.VerificationCodeExpiration.Minutes()))
// 发送短信
_, err = s.smsClient.SendMessage(ctx, content, []string{phone})
if err != nil {
s.logger.Error("发送验证码短信失败",
zap.String("phone", phone),
zap.Error(err),
)
return errors.Wrap(errors.CodeInternalError, err, "发送验证码短信失败")
}
// 存储验证码到 Redis
codeKey := constants.RedisVerificationCodeKey(phone)
err = s.redisClient.Set(ctx, codeKey, code, constants.VerificationCodeExpiration).Err()
if err != nil {
s.logger.Error("存储验证码失败",
zap.String("phone", phone),
zap.Error(err),
)
return errors.Wrap(errors.CodeInternalError, err, "存储验证码失败")
}
// 设置发送频率限制
err = s.redisClient.Set(ctx, limitKey, "1", constants.VerificationCodeRateLimit).Err()
if err != nil {
s.logger.Error("设置验证码发送频率限制失败",
zap.String("phone", phone),
zap.Error(err),
)
// 这个错误不影响主流程,只记录日志
}
s.logger.Info("验证码发送成功",
zap.String("phone", phone),
)
// 避免在日志中暴露验证码(仅在开发环境下记录)
if cfg.Logging.Development {
s.logger.Debug("验证码内容(仅开发环境)",
zap.String("phone", phone),
zap.String("code", code),
)
}
return nil
}
// VerifyCode 验证验证码
func (s *Service) VerifyCode(ctx context.Context, phone string, code string) error {
codeKey := constants.RedisVerificationCodeKey(phone)
// 从 Redis 获取验证码
storedCode, err := s.redisClient.Get(ctx, codeKey).Result()
if err == redis.Nil {
s.logger.Warn("验证码不存在或已过期",
zap.String("phone", phone),
)
return errors.New(errors.CodeInvalidParam, "验证码不存在或已过期")
}
if err != nil {
s.logger.Error("获取验证码失败",
zap.String("phone", phone),
zap.Error(err),
)
return errors.Wrap(errors.CodeInternalError, err, "获取验证码失败")
}
// 验证码比对
if storedCode != code {
s.logger.Warn("验证码错误",
zap.String("phone", phone),
)
return errors.New(errors.CodeInvalidParam, "验证码错误")
}
// 验证成功,删除验证码(防止重复使用)
err = s.redisClient.Del(ctx, codeKey).Err()
if err != nil {
s.logger.Error("删除验证码失败",
zap.String("phone", phone),
zap.Error(err),
)
// 这个错误不影响主流程,只记录日志
}
s.logger.Info("验证码验证成功",
zap.String("phone", phone),
)
return nil
}
// generateCode 生成随机验证码
func (s *Service) generateCode() (string, error) {
// 生成 6 位数字验证码
const digits = "0123456789"
code := make([]byte, constants.VerificationCodeLength)
for i := range code {
num, err := rand.Int(rand.Reader, big.NewInt(int64(len(digits))))
if err != nil {
return "", err
}
code[i] = digits[num.Int64()]
}
return string(code), nil
}