「笔记005」Go课程工程实践作业2

2024 年 11 月 28 日 星期四(已编辑)
10
这篇文章上次修改于 2024 年 11 月 28 日 星期四,可能部分内容已经不适用,如有疑问可询问作者。

「笔记005」Go课程工程实践作业2

前言

这次实现主要有关作业 1 的三个扩展功能:

  1. 用户认证授权
  2. 点赞和回复功能
  3. 分页和排序功能

一、用户认证授权实现笔记

1. 数据库设计思路

首先扩展用户表,添加认证相关字段:

-- 用户表扩展
ALTER TABLE `user` 
ADD COLUMN `password` VARCHAR(255) NOT NULL COMMENT '密码',
ADD COLUMN `email` VARCHAR(100) NOT NULL COMMENT '邮箱',
ADD COLUMN `last_login` DATETIME COMMENT '最后登录时间',
ADD COLUMN `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态 1:正常 2:禁用',
ADD UNIQUE INDEX `idx_email` (`email`);

-- 创建角色表
CREATE TABLE `role` (
    `id` INT NOT NULL AUTO_INCREMENT,
    `name` VARCHAR(50) NOT NULL COMMENT '角色名称',
    `description` VARCHAR(255) COMMENT '角色描述',
    `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 用户角色关联表
CREATE TABLE `user_role` (
    `user_id` INT NOT NULL,
    `role_id` INT NOT NULL,
    PRIMARY KEY (`user_id`, `role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

2. JWT认证实现思路

2.1 认证流程设计

  1. 用户登录 -> 验证密码 -> 生成JWT -> 返回token
  2. 用户请求 -> 验证JWT -> 解析用户信息 -> 权限验证 -> 处理请求

关键代码实现:

// AuthService 认证服务
type AuthService struct {
    userRepo    *repository.UserRepository
    redisClient *redis.Client
}

// Login 登录实现
func (s *AuthService) Login(ctx context.Context, email, password string) (*TokenResponse, error) {
    // 1. 查询用户
    user, err := s.userRepo.GetByEmail(email)
    if err != nil {
        return nil, errors.Wrap(err, "用户不存在")
    }

    // 2. 验证密码
    if !utils.ComparePassword(password, user.Password) {
        // 记录登录失败次数,防止暴力破解
        s.recordLoginFailed(ctx, email)
        return nil, errors.New("密码错误")
    }

    // 3. 生成Token
    token, err := s.generateToken(user)
    if err != nil {
        return nil, err
    }

    // 4. 更新登录时间
    user.LastLogin = time.Now()
    if err := s.userRepo.Update(user); err != nil {
        log.Printf("更新登录时间失败: %v", err)
    }

    return &TokenResponse{
        Token:     token,
        ExpiresIn: 24 * 60 * 60,
    }, nil
}

// 记录登录失败次数
func (s *AuthService) recordLoginFailed(ctx context.Context, email string) {
    key := fmt.Sprintf("login:failed:%s", email)
    count, _ := s.redisClient.Incr(ctx, key).Result()
    
    // 设置过期时间,24小时后重置
    s.redisClient.Expire(ctx, key, 24*time.Hour)
    
    // 如果失败次数过多,锁定账号
    if count >= 5 {
        s.lockAccount(email)
    }
}

3. 权限控制实现

使用RBAC模型实现权限控制,关键代码:

// RequireRole 角色验证中间件
func RequireRole(roles ...string) gin.HandlerFunc {
    return func(c *gin.Context) {
        userRoles := c.MustGet("roles").([]string)
        
        // 检查用户是否具有所需角色
        hasRole := false
        for _, required := range roles {
            for _, userRole := range userRoles {
                if required == userRole {
                    hasRole = true
                    break
                }
            }
        }
        
        if !hasRole {
            c.JSON(http.StatusForbidden, gin.H{
                "error": "权限不足",
            })
            c.Abort()
            return
        }
        
        c.Next()
    }
}

二、点赞和回复功能实现笔记

1. 点赞功能设计思路

1.1 点赞计数器设计

采用Redis + MySQL的混合存储方案:

  • Redis: 存储实时点赞数据和计数
  • MySQL: 做持久化存储

实现要点:

  1. 使用Redis事务保证原子性
  2. 异步同步数据到MySQL
  3. 定期对账数据一致性

核心代码实现:

// LikeService 点赞服务
type LikeService struct {
    redis  *redis.Client
    repo   *repository.LikeRepository
    pubsub *redis.PubSub
}

// Like 点赞
func (s *LikeService) Like(ctx context.Context, req *LikeRequest) error {
    // 1. 检查重复点赞
    liked, err := s.checkLiked(ctx, req.UserID, req.TargetID, req.TargetType)
    if err != nil {
        return err
    }
    if liked {
        return errors.New("已经点赞")
    }

    // 2. Redis事务处理
    pipe := s.redis.TxPipeline()
    
    // 记录点赞关系
    likeKey := fmt.Sprintf("like:%d:%d:%d", req.TargetType, req.TargetID, req.UserID)
    pipe.Set(ctx, likeKey, "1", 24*time.Hour)
    
    // 更新计数
    countKey := fmt.Sprintf("like:count:%d:%d", req.TargetType, req.TargetID)
    pipe.Incr(ctx, countKey)
    
    // 添加到有序集合,用于热门排序
    scoreKey := fmt.Sprintf("like:hot:%d", req.TargetType)
    pipe.ZIncrBy(ctx, scoreKey, 1, strconv.FormatUint(uint64(req.TargetID), 10))
    
    _, err = pipe.Exec(ctx)
    if err != nil {
        return err
    }

    // 3. 异步持久化到MySQL
    go s.syncToMySQL(req)
    
    return nil
}

// 异步同步到MySQL
func (s *LikeService) syncToMySQL(req *LikeRequest) {
    like := &model.Like{
        UserID:     req.UserID,
        TargetID:   req.TargetID,
        TargetType: req.TargetType,
    }
    
    if err := s.repo.Create(like); err != nil {
        log.Printf("同步点赞数据到MySQL失败: %v", err)
        // 添加到重试队列
        s.addToRetryQueue(req)
    }
}

2. 评论回复功能设计

2.1 树形结构设计思路

  1. 使用邻接表模型存储
  2. 控制评论最大深度
  3. 分页加载子评论

实现代码:

// PostService 评论服务
type PostService struct {
    db    *gorm.DB
    redis *redis.Client
}

// CreateReply 创建回复
func (s *PostService) CreateReply(ctx context.Context, req *CreateReplyRequest) error {
    // 1. 检查评论层级
    level := 1
    if req.ParentID != nil {
        parent, err := s.getPost(*req.ParentID)
        if err != nil {
            return err
        }
        level = parent.Level + 1
        if level > 3 {
            return errors.New("评论层级超过限制")
        }
    }

    // 2. 创建评论
    post := &model.Post{
        TopicID:  req.TopicID,
        Content:  req.Content,
        UserID:   req.UserID,
        ParentID: req.ParentID,
        ReplyTo:  req.ReplyTo,
        Level:    level,
    }

    return s.db.Transaction(func(tx *gorm.DB) error {
        if err := tx.Create(post).Error; err != nil {
            return err
        }
        
        // 更新评论计数
        if err := tx.Model(&model.Topic{}).
            Where("id = ?", req.TopicID).
            UpdateColumn("comment_count", gorm.Expr("comment_count + ?", 1)).
            Error; err != nil {
            return err
        }
        
        return nil
    })
}

三、分页和排序功能实现笔记

1. 分页查询设计

1.1 游标分页实现

对于大数据量场景,使用游标分页替代传统的offset分页:

// TopicCursor 游标结构
type TopicCursor struct {
    CreateTime time.Time
    ID         uint
}

// ListByCursor 游标分页查询
func (r *TopicRepository) ListByCursor(cursor *TopicCursor, limit int) ([]*model.Topic, error) {
    query := r.db.Model(&model.Topic{})
    
    if cursor != nil {
        // 使用复合条件
        query = query.Where(
            "(create_time, id) < (?, ?)",
            cursor.CreateTime,
            cursor.ID,
        )
    }
    
    var topics []*model.Topic
    err := query.Order("create_time DESC, id DESC").
        Limit(limit).
        Find(&topics).Error
    
    return topics, err
}

2. 性能优化实践

2.1 缓存策略

  1. 对热门话题列表进行缓存
  2. 使用布隆过滤器防止缓存穿透
  3. 采用延迟双删策略保证缓存一致性
// GetTopicList 获取话题列表
func (s *TopicService) GetTopicList(ctx context.Context, req *ListRequest) (*ListResponse, error) {
    // 1. 尝试从缓存获取
    cacheKey := s.generateCacheKey(req)
    if data, err := s.getFromCache(ctx, cacheKey); err == nil {
        return data, nil
    }
    
    // 2. 从数据库查询
    topics, err := s.repo.List(req.toCursor(), req.PageSize)
    if err != nil {
        return nil, err
    }
    
    // 3. 写入缓存
    response := &ListResponse{
        Data:  topics,
        Total: len(topics),
    }
    go s.setCache(ctx, cacheKey, response)
    
    return response, nil
}

实现中的注意点

1. 认证授权

  • JWT密钥要定期轮换
  • 考虑token刷新机制
  • 重要操作需要二次验证
  • 注意防范暴力破解

2. 点赞功能

  • Redis的WATCH命令要注意超时
  • 点赞数据同步要考虑失败重试
  • 热门内容计算要异步处理
  • 注意防刷限制

3. 评论功能

  • 评论嵌套层级要限制
  • 大量评论时要分批加载
  • 评论删除要考虑级联
  • 注意XSS防护

4. 分页查询

  • 大数据量使用游标分页
  • 合理使用索引
  • 注意缓存更新时机
  • 考虑数据一致性

使用社交账号登录

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...