Golang 14 gin web项目实战 IM系统(3) 用户与群组
41
2025/7/21 15:00 - 2025/7/21 23:00
这一部分比较简单,并不会贴出所有代码,只挑关键的,另外测试api采用apifox软件,swag测试过于麻烦。
用户功能
User struct
User.go,指针代表该字段可以为null而不是存零值:
type User struct {
gorm.Model
Username string `json:"username"` // 用户名
Password string `json:"password"` // 用户密码(加密后)
Nickname *string `json:"nickname"` // 昵称
Phone *string `json:"phone"` // 用户手机号
Email *string `json:"email"` // 用户邮箱
Avatar *string `json:"avatar,omitempty"` // 用户头像URL(可选)
ClientIp string `json:"client_ip"` // 客户端IP地址
ClientPort string `json:"client_port"` // 客户端端口号
LoginTime int64 `json:"login_time"` // 最近一次登录时间
HeartbeatTime int64 `json:"heartbeat_time"` // 最近一次心跳时间
LogoutTime int64 `json:"logout_time"` // 最近一次登出时间
Status Status `json:"status"` // 用户状态(如激活、禁用等)
OnlineStatus OnlineStatus `json:"online_status"` // 用户在线状态(如在线、离线、忙碌等)
DeviceInfo *string `json:"device_info"` // 客户端设备信息
}
接口
路径省略了前缀 http://localhost/api/v1
| 接口名 | 请求路径 | 参数 | 响应体 |
|---|---|---|---|
| 注册 | /user/register | { "username":string, "password":string, "rePassword":string } |
200 { "code":200, "message":"ok" "data":nil } |
| 登录 | /user/login | { "username":string, "password":string } |
200 { "code":200, "message":"ok" "data":token } |
| 更新基本信息 | /user/update |
- 以其中注册接口为例:
创建request:
type RegisterRequest struct {
Username *string `json:"username" binding:"required"` // 用户名
Password *string `json:"password" binding:"required"` // 密码
RePassword *string `json:"re_password" binding:"required"` // 确认密码
}
控制器中接收数据(可追加校验逻辑,比如账号密码长度等)并调用服务:
func (con UserController) Register(c *gin.Context) {
registerRequest := &model.RegisterRequest{}
if err := c.ShouldBindBodyWithJSON(registerRequest); err != nil {
con.Error(c, "params error")
return
}
userService := service.GetUserService()
if err := userService.Register(registerRequest.Username, registerRequest.Password, registerRequest.RePassword); err != nil {
con.Error(c, err.Error())
return
}
con.Success(c)
return
}
服务处理注册请求:
// Register 注册
func (u *UserService) Register(username, password, rePassword string) (err error) {
if password != rePassword {
return errors.New("密码不一致")
}
userRepository := repository.GetUserRepository()
user, err := userRepository.GetByName(username)
if err != nil {
logUtil.Errorf("GetUserByName error: %v", err)
return err
}
if user != nil {
logUtil.Warnf("用户(%v)已存在", username)
return errors.New(fmt.Sprintf("用户(%v)已存在", username))
}
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return err
}
user = &models.User{Username: username, Password: string(hashedPassword), Nickname: username}
err = userRepository.Save(user)
if err != nil {
logUtil.Errorf("保存用户失败: %v", err)
return err
}
return nil
}
2. 登录、更新同理
新用到的库
- 加密
go get golang.org/x/crypto/bcrypt
- 校验
go get github.com/go-playground/validator/v10
群组功能
Group Struct
package model
import (
"gorm.io/gorm"
)
type Group struct {
gorm.Model
Code string `json:"code"` //群号
Name string `json:"name"` //群组名称
Desc string `json:"description"` //群组简介
OwnerId uint `json:"owner_id"` //群主ID
MaxNum int `json:"max_num"` //群组最大人数
Status Status `json:"status" ` //群组状态(1=正常,0=关闭)
}
func (m *Group) TableName() string {
return "groups"
}
GroupMember Struct
package model
import "gorm.io/gorm"
type GroupMember struct {
gorm.Model
GroupId uint `json:"group_id"` //群ID
MemberId uint `json:"member_id"` //成员ID
GNickName string `json:"g_nick_name"` //群昵称
Role Role `json:"role"` //成员角色(0=普通成员,1=群主,2=管理员)
Status Status `json:"status"` //成员状态(0禁言1正常)
}
func (m *GroupMember) TableName() string {
return "group_members"
}
接口
| 接口名 | 请求方式 | 请求路径 | 参数 | 响应体 |
|---|---|---|---|---|
| 创建群 | post | /group/create | { "user_id": 3, "name": "Go 语言爱好者交流群", "max_num":5, "member_list": [3,4] } |
200 { "code":200, "message":"ok" "data":nil } |
| 加入群 | get | /group/join | user_id:jwt获取 group_id:QueryParams |
200 { "code":200, "message":"ok" "data":nil } |
| 退出群 | get | /group/quit | user_id:jwt获取 group_id:QueryParams |
200 { "code":200, "message":"ok" "data":nil } |
部分代码 GroupService
package service
import (
"errors"
"go-chat/internal/db"
"go-chat/internal/model"
request "go-chat/internal/model/request"
"go-chat/internal/repository"
"go-chat/internal/utils/idUtil"
"gorm.io/gorm"
"sync"
)
type GroupService struct{}
var (
groupServiceInstance *GroupService
groupOnce sync.Once
)
func GetGroupService() *GroupService {
groupOnce.Do(func() {
groupServiceInstance = &GroupService{}
})
return groupServiceInstance
}
// Create 创建群组
func (s GroupService) Create(req *request.GroupCreateRequest) error {
err := db.Mysql.Transaction(func(tx *gorm.DB) error {
groupRepo := repository.GetGroupRepository()
groupMemberRepo := repository.GetGroupMemberRepository()
userRepo := repository.GetUserRepository()
maxAttempts := 3
code := idUtil.GenerateId()
for attempts := 0; attempts < maxAttempts; attempts++ {
if exists := groupRepo.ExistsByCode(code, tx); !exists {
break
}
code = idUtil.GenerateId()
}
if exists := groupRepo.ExistsByCode(code, tx); exists {
return errors.New("error code, please try again")
}
group := &model.Group{
OwnerId: req.UserId,
Name: req.Name,
MaxNum: req.MaxNum,
Code: code,
Desc: "群主很懒,什么也没留下~",
Status: model.Enable,
}
if err := groupRepo.Save(group, tx); err != nil {
return err
}
if req.MemberList != nil && len(*req.MemberList) > 0 {
groupMemberList := make([]*model.GroupMember, len(*req.MemberList))
idNickNameMap, err := userRepo.GetNickNamesByIds(*req.MemberList, tx)
if err != nil {
return err
}
for i, memberId := range *req.MemberList {
nickname := idNickNameMap[memberId]
var role model.Role
if req.UserId == memberId {
role = model.Owner // 群主角色
} else {
role = model.Member // 普通成员角色
}
groupMemberList[i] = &model.GroupMember{
GroupId: group.ID,
MemberId: memberId,
GNickName: nickname,
Role: role,
Status: model.Enable,
}
}
if err := groupMemberRepo.SaveBatch(groupMemberList, tx); err != nil {
return err
}
}
return nil
})
if err != nil {
return err
}
return nil
}
func (s GroupService) Join(groupId uint, userId uint) error {
return db.Mysql.Transaction(func(tx *gorm.DB) error {
userRepo := repository.GetUserRepository()
groupMemberRepo := repository.GetGroupMemberRepository()
exists := groupMemberRepo.ExistsByGroupIdAndUserId(groupId, userId, tx)
if exists {
return errors.New("用户已加入该群组")
}
rejoin := groupMemberRepo.RejoinGroupIfDeleted(groupId, userId, tx)
if rejoin {
return nil
}
nickname, err := userRepo.GetNickNamesById(userId, tx)
if err != nil {
return err
}
member := &model.GroupMember{
GroupId: groupId,
MemberId: userId,
GNickName: nickname,
Role: model.Member,
Status: model.Enable,
}
if err := groupMemberRepo.Save(member, tx); err != nil {
return err
}
return nil
})
}
func (s GroupService) Quit(groupId uint, memberId uint) error {
memberRepository := repository.GetGroupMemberRepository()
return memberRepository.DeleteByGroupIdAndUserId(groupId, memberId)
}
新用到库
- 随机id,用来当群号
go get github.com/jaevor/go-nanoid
简单支撑私聊、群聊消息的的基本模块,群组有更多操作但是与消息无关先放一放
浙公网安备 33010602011771号