「笔记005」Go课程工程实践作业2
前言
这次实现主要有关作业 1 的三个扩展功能:
- 用户认证授权
- 点赞和回复功能
- 分页和排序功能
一、用户认证授权实现笔记
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 认证流程设计
- 用户登录 -> 验证密码 -> 生成JWT -> 返回token
- 用户请求 -> 验证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: 做持久化存储
实现要点:
- 使用Redis事务保证原子性
- 异步同步数据到MySQL
- 定期对账数据一致性
核心代码实现:
// 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 树形结构设计思路
- 使用邻接表模型存储
- 控制评论最大深度
- 分页加载子评论
实现代码:
// 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 缓存策略
- 对热门话题列表进行缓存
- 使用布隆过滤器防止缓存穿透
- 采用延迟双删策略保证缓存一致性
// 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. 分页查询
- 大数据量使用游标分页
- 合理使用索引
- 注意缓存更新时机
- 考虑数据一致性