goframe框架签到系统项目开发(分布式 ID 生成器、雪花算法、抽离业务逻辑到service层)

文章目录

  • 注册流程
  • 登录流程
  • 用户认证流程
  • [分布式 ID 生成器](#分布式 ID 生成器)
    • [分布式 ID 的特点](#分布式 ID 的特点)
  • 可能会问的问题
    • [1. 为什么不直接使用数据库主键 ID 做用户 ID?](#1. 为什么不直接使用数据库主键 ID 做用户 ID?)
    • [2. 为什么不使用 UUID(Universally Unique Identifier)/ GUID(Globally Unique Identifier)?](#2. 为什么不使用 UUID(Universally Unique Identifier)/ GUID(Globally Unique Identifier)?)
  • [snowflake 算法介绍](#snowflake 算法介绍)
    • [SnowFlake 算法在同一毫秒内最多可以生成多少个全局唯一 ID 呢?](#SnowFlake 算法在同一毫秒内最多可以生成多少个全局唯一 ID 呢?)
  • [snowflake 的 Go 实现](#snowflake 的 Go 实现)
  • 测试

注册流程

提交注册信息 → 参数校验 → 入库 → 注册成功

登录流程

提交登录信息 → 参数校验 → 查询数据库 → 登陆成功 → 返回 Token

用户认证流程

在请求中携带 token → 后端认证中间件校验 → 校验成功 → 返回数据

分布式 ID 生成器

分布式 ID 的特点

  • 全局唯一性:不能出现有重复的 ID 标识,这是基本要求。
  • 递增性:确保生成 ID 对于用户或业务是递增的。
  • 高可用性:确保任何时候都能生成正确的 ID
  • 高性能:在高并发的环境下依然表现良好

不仅仅是用于用户 ID,实际互联网中有很多场景都需要能够生成类似 MySQL 自增 ID 这样不断增大,同时又不会重复的 ID,以支持业务中的高并发场景。

比较典型的场景有:

  • 电商促销时短时间内会有大量的订单涌入到系统,比如每秒 10w+
  • 明星出轨时微博短时间内会产生大量的相关微博转发和评论消息。

在这些业务场景下将数据插入数据库之前,我们需要给这些订单和消息先分配一个唯一 ID,然后再保存到数据库中。对这个 ID 的要求是希望其中能带有一些时间信息,这样即使我们后端的系统对消息进行了分库分表,也能够以时间顺序对这些消息进行排序

可能会问的问题

uid: 781261132115

uid: 78943213101

req → uid → uid % 100 →

  • userinfo_15
  • userinfo_01

1. 为什么不直接使用数据库主键 ID 做用户 ID?

使用数据库自增 ID 作为用户 ID,无法满足超大用户量规模和分布式架构的需求。

分布式 ID 生成系统本身是一个高度可靠、容错的分布式服务。

2. 为什么不使用 UUID(Universally Unique Identifier)/ GUID(Globally Unique Identifier)?

snowflake 算法介绍

雪花算法,它是 Twitter 开源的由 64 位整数组成的分布式 ID,性能较高,并且在单机上递增

通常一个 64 位的 ID 会被划分为以下几个部分:

SnowFlake 算法在同一毫秒内最多可以生成多少个全局唯一 ID 呢?

同一毫秒的 ID 数量 = 1024 × 4096 = 4194304

snowflake 的 Go 实现

1. bwmarrin/snowflake

github.com/bwmarrin/snowflake 是一个相当轻量化的 snowflake Go 实现。

产生的 64 位 ID 结构如下所示:

安装依赖

go 复制代码
go get github.com/bwmarrin/snowflake

使用示例:

go 复制代码
package main

import (
    "fmt"
    "time"

    "github.com/bwmarrin/snowflake"
)

func main() {
    // 设置自定义的系统起始时间戳
    startTime, _ := time.Parse(time.DateOnly, "2025-01-01")
    snowflake.Epoch = startTime.UnixMilli()

    // 创建 1 号节点,集群中不能使用重复的 node 序号
    node, err := snowflake.NewNode(1)
    if err != nil {
        fmt.Println(err)
        return
    }

    // 生成一个 snowflake ID
    id := node.Generate()

    // 打印不同展示形式的 ID
    fmt.Printf("Int64  ID: %d\n", id)
    fmt.Printf("String ID: %s\n", id)
    fmt.Printf("Base2  ID: %s\n", id.Base2())
    fmt.Printf("Base64 ID: %s\n", id.Base64())
}

2. sonyflake

sonyflake 是 Sony 公司开源的一个项目,基本思路和 snowflake 差不多,不过位分配上稍有不同:

这个库生成的 ID 中时间戳只用了 39 个 bit,但时间的单位变成了 10ms,

所以理论上比 41 位表示的时间还要久(174 年)。

Sequence ID 和之前的定义一致,Machine ID 其实就是 Node ID。

安装依赖:

go 复制代码
go get github.com/sony/sonyflake/v2

sonyflake 库有以下配置参数:

go 复制代码
type Settings struct {
    BitsSequence   int
    BitsMachineID  int
    TimeUnit       time.Duration
    StartTime      time.Time
    MachineID      func() (int, error)
    CheckMachineID func(int) bool
}

其中:

  • StartTime 选项和我们之前的 Epoch 差不多,如果不设置的话,
    默认是从 2014-09-01 00:00:00 +0000 UTC 开始。
  • MachineID 可以由用户自定义的函数,如果用户不定义的话,
    会默认将本机 IP 的低 16 位作为 machine id。
  • CheckMachineID 是由用户提供的检查 MachineID 是否冲突的函数。

使用示例:

go 复制代码
package main

import (
    "fmt"
    "github.com/sony/sonyflake/v2"
    "time"
)

func main() {
    st, _ := time.Parse(time.DateOnly, "2025-01-01")
    settings := sonyflake.Settings{
        StartTime: st,
    }
    sonyFlake, err := sonyflake.New(settings)
    if err != nil {
        panic(err)
    }

    id, err := sonyFlake.NextID()
    fmt.Printf("id:%v err:%v\n", id, err)
}

为用户注册实现层添加雪花算法

go 复制代码
package userinfo

import (
	"context"
	"time"

	"github.com/gogf/gf/crypto/gmd5"
	_ "github.com/gogf/gf/v2/errors/gcode"
	"github.com/gogf/gf/v2/errors/gerror"
	"github.com/gogf/gf/v2/frame/g"

	v1 "backend/api/userinfo/v1"
	"backend/internal/dao"
	"backend/internal/model/entity"

	"github.com/sony/sonyflake/v2"
)

const (
	defaultAvatar = "https://avatars.githubusercontent.com/u/51045272?v=4"
)

// 建议:在包级别初始化一次(比如 init() 或 main 启动时)
var sf *sonyflake.Sonyflake

func init() {
	st, err := time.Parse(time.DateOnly, "2025-11-01") // 用过去时间
	if err != nil {
		panic(err) // 这里只在启动阶段 panic 可以接受
	}
	sf, err = sonyflake.New(sonyflake.Settings{StartTime: st})
	if err != nil {
		panic(err)
	}
}

// 用户注册流程
func (c *ControllerV1) Create(ctx context.Context, req *v1.CreateReq) (*v1.CreateRes, error) {
	// 参数校验(框架已经做完了)
	// 创建用户,入库
	// st, _ := time.Parse(time.DateOnly, "2025-11-01")
	// settings := sonyflake.Settings{
	// 	StartTime: st,
	// }

	// sonyFlake, err := sonyflake.New(settings)
	// if err != nil {
	// 	panic(err)
	// }

	// userID, err := sonyFlake.NextID() // id
	// fmt.Printf("id:%v err:%v\n", userID, err)

	userID, err := sf.NextID()
	if err != nil {
		g.Log().Errorf(ctx, "生成用户ID失败: %v", err)
		return nil, gerror.Wrap(err, "生成用户ID失败")
	}

	// 拿一条记录/返回给业务/插入一整条数据 → 用 entity.Userinfo
	// 写查询条件、更新字段(部分更新) → 用 do.Userinfo
	newUserInfo := entity.Userinfo{
		UserId:   uint64(userID), // 使用雪花算法生成唯一ID
		Username: req.Username,
		// Password: req.Password, // 是不是需要对用户输入的密码进行加密
		Password: gmd5.MustEncryptString(req.Password), // 对字符串 s 做 MD5, 返回 MD5 的十六进制字符串
		// 练习可用;生产换 bcrypt

		Email:  req.Email,
		Avatar: defaultAvatar, // 简化注册流程,一般使用默认头像,后续支持用户在个人中心上传头像
	}

	// id, err := dao.Userinfo.Ctx(ctx).InsertAndGetId(newUserInfo)

	_, err = dao.Userinfo.Ctx(ctx).Insert(newUserInfo)
	if err != nil {
		g.Log().Errorf(ctx, "创建用户失败:%v", err)
		return nil, gerror.Wrap(err, "创建用户失败")
	}

	// 返回结果
	return &v1.CreateRes{
		UserId:   uint64(userID),
		Username: newUserInfo.Username,
	}, nil
}


抽离业务逻辑到service层

go 复制代码
package userinfo

import (
	// "backend/internal/dao"
	"backend/internal/model"
	// "backend/internal/model/entity"
	"context"
	// "time"
	// "github.com/gogf/gf/crypto/gmd5"
	// "github.com/gogf/gf/v2/errors/gerror"
	// "github.com/gogf/gf/v2/frame/g"
	// "github.com/sony/sonyflake/v2"
)

// 把用户服务抽象成一个接口, 列出来所需要实现的方法
type UserInfoService interface {
	Create(ctx context.Context, input *model.CreateUserInput) (*model.CreateUserOutput, error)
}
go 复制代码
package impl

import (
	"backend/internal/dao"
	"backend/internal/model"
	"backend/internal/model/entity"
	"backend/internal/service/userinfo"
	"context"
	"time"

	"github.com/gogf/gf/crypto/gmd5"
	"github.com/gogf/gf/v2/errors/gerror"
	"github.com/gogf/gf/v2/frame/g"
	"github.com/sony/sonyflake/v2"
)

// 用户相关业务逻辑
// 定义一个结构体, 实现UserInfoService接口
type UserInfo struct{}

// func New() *UserInfo {
// 	return &UserInfo{}
// }

func New() userinfo.UserInfoService {
	return &UserInfo{}
}

// 建议:在包级别初始化一次(比如 init() 或 main 启动时)
var sf *sonyflake.Sonyflake

const (
	defaultAvatar = "https://avatars.githubusercontent.com/u/51045272?v=4"
)

func init() {
	st, err := time.Parse(time.DateOnly, "2025-11-01") // 用过去时间
	if err != nil {
		panic(err) // 这里只在启动阶段 panic 可以接受
	}
	sf, err = sonyflake.New(sonyflake.Settings{StartTime: st})
	if err != nil {
		panic(err)
	}
}

// Create 创建用户
// func (u *UserInfo) Create(ctx context.Context, username, password, email string) error {
func (u *UserInfo) Create(ctx context.Context, input *model.CreateUserInput) (*model.CreateUserOutput, error) {
	// 1. 判断用户名是否已经存在(根据用户名查重)
	exist, err := dao.Userinfo.Ctx(ctx).
		Where(dao.Userinfo.Columns().Username, input.Username).
		Exist()
	if err != nil {
		g.Log().Errorf(ctx, "查询用户是否存在失败: %v", err)
		return nil, err
	}

	if exist {
		return nil, gerror.New("用户已存在")
	}

	// 2. 生成唯一 id
	userID, err := sf.NextID()
	if err != nil {
		g.Log().Errorf(ctx, "生成用户ID失败: %v", err)
		return nil, gerror.Wrap(err, "生成用户ID失败")
	}

	// 3. 创建用户
	// 创建用户,入库
	newUserInfo := entity.Userinfo{
		UserId:   uint64(userID), // 使用雪花算法生成唯一ID
		Username: input.Username,
		// Password: req.Password, // 是不是需要对用户输入的密码进行加密
		Password: gmd5.MustEncryptString(input.Password), // 对字符串 s 做 MD5, 返回 MD5 的十六进制字符串
		// 练习可用;生产换 bcrypt

		Email:  input.Email,
		Avatar: defaultAvatar, // 简化注册流程,一般使用默认头像,后续支持用户在个人中心上传头像
	}

	// id, err := dao.Userinfo.Ctx(ctx).InsertAndGetId(newUserInfo)

	_, err = dao.Userinfo.Ctx(ctx).Insert(newUserInfo)
	if err != nil {
		g.Log().Errorf(ctx, "创建用户失败:%v", err)
		return nil, gerror.Wrap(err, "创建用户失败")
	}
	// 4. 返回结果
	return &model.CreateUserOutput{
		UserId:   uint64(userID),
		Username: input.Username,
	}, nil
}
go 复制代码
package model

// 定义 controller 层 与 service 层 之间交互的数据
// gf 框架推荐使用 input/output 结构体封装交互的数据

type CreateUserInput struct {
	Username string `json:"username"`
	Password string `json:"password"`
	Email    string `json:"email"`
}

type CreateUserOutput struct {
	UserId   uint64 `json:"userId"`
	Username string `json:"username"`
}
go 复制代码
package userinfo

import (
	"context"
	_ "time"

	"backend/internal/model"

	_ "github.com/gogf/gf/crypto/gmd5"
	_ "github.com/gogf/gf/v2/errors/gcode"
	_ "github.com/gogf/gf/v2/errors/gerror"
	"github.com/gogf/gf/v2/frame/g"

	v1 "backend/api/userinfo/v1"
	_ "backend/internal/dao"
	_ "backend/internal/model/entity"

	_ "github.com/sony/sonyflake/v2"
)

// const (
// 	defaultAvatar = "https://avatars.githubusercontent.com/u/51045272?v=4"
// )

// // 建议:在包级别初始化一次(比如 init() 或 main 启动时)
// var sf *sonyflake.Sonyflake

// func init() {
// 	st, err := time.Parse(time.DateOnly, "2025-11-01") // 用过去时间
// 	if err != nil {
// 		panic(err) // 这里只在启动阶段 panic 可以接受
// 	}
// 	sf, err = sonyflake.New(sonyflake.Settings{StartTime: st})
// 	if err != nil {
// 		panic(err)
// 	}
// }

// 用户注册流程
func (c *ControllerV1) Create(ctx context.Context, req *v1.CreateReq) (*v1.CreateRes, error) {
	// 参数校验(框架已经做完了)

	// st, _ := time.Parse(time.DateOnly, "2025-11-01")
	// settings := sonyflake.Settings{
	// 	StartTime: st,
	// }

	// sonyFlake, err := sonyflake.New(settings)
	// if err != nil {
	// 	panic(err)
	// }

	// userID, err := sonyFlake.NextID() // id
	// fmt.Printf("id:%v err:%v\n", userID, err)

	// userID, err := sf.NextID()
	// if err != nil {
	// 	g.Log().Errorf(ctx, "生成用户ID失败: %v", err)
	// 	return nil, gerror.Wrap(err, "生成用户ID失败")
	// }

	// 拿一条记录/返回给业务/插入一整条数据 → 用 entity.Userinfo
	// 写查询条件、更新字段(部分更新) → 用 do.Userinfo
	// newUserInfo := entity.Userinfo{
	// 	UserId:   uint64(userID), // 使用雪花算法生成唯一ID
	// 	Username: req.Username,
	// 	// Password: req.Password, // 是不是需要对用户输入的密码进行加密
	// 	Password: gmd5.MustEncryptString(req.Password), // 对字符串 s 做 MD5, 返回 MD5 的十六进制字符串
	// 	// 练习可用;生产换 bcrypt

	// 	Email:  req.Email,
	// 	Avatar: defaultAvatar, // 简化注册流程,一般使用默认头像,后续支持用户在个人中心上传头像
	// }

	// // id, err := dao.Userinfo.Ctx(ctx).InsertAndGetId(newUserInfo)

	// _, err = dao.Userinfo.Ctx(ctx).Insert(newUserInfo)
	// if err != nil {
	// 	g.Log().Errorf(ctx, "创建用户失败:%v", err)
	// 	return nil, gerror.Wrap(err, "创建用户失败")
	// }

	// 返回结果
	// return &v1.CreateRes{
	// 	UserId:   uint64(userID),
	// 	Username: newUserInfo.Username,
	// }, nil

	// 参数校验(框架已经做完了)
	input := model.CreateUserInput{
		Username: req.Username,
		Password: req.Password,
		Email:    req.Email,
	}

	// 调用service层的逻辑
	output, err := c.svc.Create(ctx, &input)
	if err != nil {
		g.Log().Errorf(ctx, "Create user failed: %v", err)
		return nil, err
	}

	// 返回结果
	return &v1.CreateRes{
		UserId:   output.UserId,
		Username: output.Username,
	}, nil
}
go 复制代码
// =================================================================================
// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish.
// =================================================================================

package userinfo

import (
	"backend/api/userinfo"                      // api层的user包
	srvUser "backend/internal/service/userinfo" // service层的user包
	"backend/internal/service/userinfo/impl"
)

type ControllerV1 struct {
	// 用户信息服务
	svc srvUser.UserInfoService
}

func NewV1() userinfo.IUserinfoV1 {
	return &ControllerV1{
		svc: impl.New(),
	}
}

测试


之后我会持续更新,如果喜欢我的文章,请记得一键三连哦,点赞关注收藏,你的每一个赞每一份关注每一次收藏都将是我前进路上的无限动力 !!!↖(▔▽▔)↗感谢支持!

相关推荐
AIpanda8882 小时前
当智能化工具应用于企业,如何借助AI销冠系统提升工作效率?
算法
航Hang*2 小时前
第3章:复习篇——第3节:数据查询与统计
数据库·笔记·sql·mysql
only°夏至besos2 小时前
基于 Dinky + FlinkSQL + Flink CDC 同步 MySQL 数据到 Elasticsearch、Kafka
mysql·elasticsearch·flink
进击的小头2 小时前
01_嵌入式C与控制理论入门:从原理到MCU实战落地
c语言·单片机·算法
么么...2 小时前
SQL 学习指南:从零开始掌握DQL结构化查询语言
数据库·经验分享·笔记·sql
what_20182 小时前
list 对象里面 嵌套list对象,对象的属性 有浮点数,list<浮点数> 对list对象求均值
算法·均值算法
ekkcole2 小时前
mysql查看数据库指定字段存在哪个表
数据库·mysql
Austindatabases2 小时前
OceanBase SeekDB SQL优化案例---MySQL在客户端会没有市场的
数据库·sql·mysql·adb·oceanbase
wanghowie2 小时前
01.09 Java基础篇|算法与数据结构实战
java·数据结构·算法