在项目宪章中新增第九条原则"数据库设计原则",明确禁止使用数据库外键约束和ORM关联标签。 主要变更: - 新增原则IX:数据库设计原则(Database Design Principles) - 强制要求:数据库表不得使用外键约束 - 强制要求:GORM模型不得使用ORM关联标签(foreignKey、hasMany等) - 强制要求:表关系必须通过ID字段手动维护 - 强制要求:关联数据查询必须显式编写,避免ORM魔法 - 强制要求:时间字段由GORM处理,不使用数据库触发器 设计理念: - 提升业务逻辑灵活性(无数据库约束限制) - 优化高并发性能(无外键检查开销) - 增强代码可读性(显式查询,无隐式预加载) - 简化数据库架构和迁移流程 - 支持分布式和微服务场景 版本升级:2.3.0 → 2.4.0(MINOR)
156 lines
4.2 KiB
Go
156 lines
4.2 KiB
Go
package task
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/bytedance/sonic"
|
|
"github.com/hibiken/asynq"
|
|
"github.com/redis/go-redis/v9"
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
|
)
|
|
|
|
// EmailPayload 邮件任务载荷
|
|
type EmailPayload struct {
|
|
RequestID string `json:"request_id"`
|
|
To string `json:"to"`
|
|
Subject string `json:"subject"`
|
|
Body string `json:"body"`
|
|
CC []string `json:"cc,omitempty"`
|
|
Attachments []string `json:"attachments,omitempty"`
|
|
}
|
|
|
|
// EmailHandler 邮件任务处理器
|
|
type EmailHandler struct {
|
|
redis *redis.Client
|
|
logger *zap.Logger
|
|
}
|
|
|
|
// NewEmailHandler 创建邮件任务处理器
|
|
func NewEmailHandler(redis *redis.Client, logger *zap.Logger) *EmailHandler {
|
|
return &EmailHandler{
|
|
redis: redis,
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
// HandleEmailSend 处理邮件发送任务
|
|
func (h *EmailHandler) HandleEmailSend(ctx context.Context, task *asynq.Task) error {
|
|
// 解析任务载荷
|
|
var payload EmailPayload
|
|
if err := sonic.Unmarshal(task.Payload(), &payload); err != nil {
|
|
h.logger.Error("解析邮件任务载荷失败",
|
|
zap.Error(err),
|
|
zap.String("task_id", task.ResultWriter().TaskID()),
|
|
)
|
|
return asynq.SkipRetry // JSON 解析失败不重试
|
|
}
|
|
|
|
// 验证载荷
|
|
if err := h.validatePayload(&payload); err != nil {
|
|
h.logger.Error("邮件任务载荷验证失败",
|
|
zap.Error(err),
|
|
zap.String("request_id", payload.RequestID),
|
|
)
|
|
return asynq.SkipRetry // 参数错误不重试
|
|
}
|
|
|
|
// 幂等性检查:使用 Redis 锁
|
|
lockKey := constants.RedisTaskLockKey(payload.RequestID)
|
|
locked, err := h.acquireLock(ctx, lockKey)
|
|
if err != nil {
|
|
h.logger.Error("获取任务锁失败",
|
|
zap.Error(err),
|
|
zap.String("request_id", payload.RequestID),
|
|
)
|
|
return err // 锁获取失败,可以重试
|
|
}
|
|
if !locked {
|
|
h.logger.Info("任务已执行,跳过(幂等性)",
|
|
zap.String("request_id", payload.RequestID),
|
|
zap.String("to", payload.To),
|
|
)
|
|
return nil // 已执行,跳过
|
|
}
|
|
|
|
// 记录任务开始执行
|
|
h.logger.Info("开始处理邮件发送任务",
|
|
zap.String("request_id", payload.RequestID),
|
|
zap.String("to", payload.To),
|
|
zap.String("subject", payload.Subject),
|
|
zap.Int("cc_count", len(payload.CC)),
|
|
zap.Int("attachments_count", len(payload.Attachments)),
|
|
)
|
|
|
|
// 执行邮件发送(模拟)
|
|
if err := h.sendEmail(ctx, &payload); err != nil {
|
|
h.logger.Error("邮件发送失败",
|
|
zap.Error(err),
|
|
zap.String("request_id", payload.RequestID),
|
|
zap.String("to", payload.To),
|
|
)
|
|
return err // 发送失败,可以重试
|
|
}
|
|
|
|
// 记录任务完成
|
|
h.logger.Info("邮件发送成功",
|
|
zap.String("request_id", payload.RequestID),
|
|
zap.String("to", payload.To),
|
|
)
|
|
|
|
return nil
|
|
}
|
|
|
|
// validatePayload 验证邮件载荷
|
|
func (h *EmailHandler) validatePayload(payload *EmailPayload) error {
|
|
if payload.RequestID == "" {
|
|
return fmt.Errorf("request_id 不能为空")
|
|
}
|
|
if payload.To == "" {
|
|
return fmt.Errorf("收件人不能为空")
|
|
}
|
|
if !strings.Contains(payload.To, "@") {
|
|
return fmt.Errorf("邮箱格式无效")
|
|
}
|
|
if payload.Subject == "" {
|
|
return fmt.Errorf("邮件主题不能为空")
|
|
}
|
|
if payload.Body == "" {
|
|
return fmt.Errorf("邮件正文不能为空")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// acquireLock 获取 Redis 锁(幂等性)
|
|
func (h *EmailHandler) acquireLock(ctx context.Context, key string) (bool, error) {
|
|
// 使用 SetNX 实现分布式锁
|
|
// 过期时间 24 小时,防止锁永久存在
|
|
result, err := h.redis.SetNX(ctx, key, "1", 24*time.Hour).Result()
|
|
if err != nil {
|
|
return false, fmt.Errorf("设置 Redis 锁失败: %w", err)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// sendEmail 发送邮件(实际实现需要集成 SMTP 或邮件服务)
|
|
func (h *EmailHandler) sendEmail(ctx context.Context, payload *EmailPayload) error {
|
|
// TODO: 实际实现中需要集成邮件发送服务
|
|
// 例如:使用 SMTP、SendGrid、AWS SES 等
|
|
|
|
// 模拟发送延迟
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
// 这里仅作演示,实际应用中需要调用真实的邮件发送 API
|
|
h.logger.Debug("模拟邮件发送",
|
|
zap.String("to", payload.To),
|
|
zap.String("subject", payload.Subject),
|
|
zap.Int("body_length", len(payload.Body)),
|
|
)
|
|
|
|
return nil
|
|
}
|