「从0到1搭建一个IM项目」好友关系模块开发之关系结构的设计
[toc]
概况
前面部分我们简单了,用户模块发一下基本功能的开发,现在我们继续完善IM系统功能,作为聊天系统,用户之间必须存在着一定关系,如陌生人、好友、群友等,本篇的重点将来介绍如何设计用户关系结构。
到目前为止,项目目录结构:
HiChat
├── common //放置公共文件
│
├── config //做配置文件
│
├── dao//数据库crud
│ |——user.go
|
├── global //放置各种连接池,配置等
│ |——global.go
|
├── initialize //项目初始化文件
│ |——db.go
| |——logger.go
|
├── middlewear //放置web中间件
│
├── models //数据库表设计
│ |——user_basic.go
|
├── router //路由
| |——router.go
│
├── service //对外api
| |——user.go
│
├── test //测试文件
│
├── main.go //项目入口
├── go.mod //项目依赖管理
├── go.sum //项目依赖管理
用户关系表设计
首先每一张表都需要:
type Model struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}
对于一个用户的好友关系,这里我们就以最简单的方式进行设计
谁的用户关系
对应的好友
关系类型
关系描述
结构体:
package models
type Relation struct {
Model
OwnerId uint //谁的关系信息
TargetID uint //对应的谁
Type int //关系类型: 1表示好友关系 2表示群关系
Desc string //描述
}
func (r *Relation) RelTableName() string {
return "relation"
}
生成表发方式在用户模块开发中已经介绍过,这里不做过多赘述
dao层的开发
好友列表
有了好友关系表,现在来完成好友列表功能的开发,在dao目录下新建relation.go文件
//FriendList 获取好友列表
func FriendList(userId uint) (*[]models.UserBasic, error) {
relation := make([]models.Relation, 0)
if tx := global.DB.Where("owner_id = ? and type=1", userId).Find(&relation); tx.RowsAffected == 0 {
zap.S().Info("未查询到Relation数据")
return nil, errors.New("未查到好友关系")
}
userID := make([]uint, 0)
for _, v := range relation {
userID = append(userID, v.TargetID)
}
user := make([]models.UserBasic, 0)
if tx := global.DB.Where("id in ?", userID).Find(&user); tx.RowsAffected == 0 {
zap.S().Info("未查询到Relation好友关系")
return nil, errors.New("未查到好友")
}
return &user, nil
}
通过id添加好友
添加好友是一个双向的过程,一旦添加双发都将存在好友关系(数据库进行两次添加记录),不能出现任何问题,导致一方添加成功,而另一方添加失败的情况,所以这里需要使用数据库的事务特性(全部成功则修改数据库内容,否则不会改变数据库数据)解决问题。
//AddFriend 加好友
func AddFriend(userID, TargetId uint) (int, error) {
if userID == TargetId {
return -2, errors.New("userID和TargetId相等")
}
//通过id查询用户
targetUser, err := FindUserID(TargetId)
if err != nil {
return -1, errors.New("未查询到用户")
}
if targetUser.ID == 0 {
zap.S().Info("未查询到用户")
return -1, errors.New("未查询到用户")
}
relation := models.Relation{}
if tx := global.DB.Where("owner_id = ? and target_id = ? and type = 1", userID, TargetId).First(&relation); tx.RowsAffected == 1 {
zap.S().Info("该好友存在")
return 0, errors.New("好友已经存在")
}
if tx := global.DB.Where("owner_id = ? and target_id = ? and type = 1", TargetId, userID).First(&relation); tx.RowsAffected == 1 {
zap.S().Info("该好友存在")
return 0, errors.New("好友已经存在")
}
//开启事务
tx := global.DB.Begin()
relation.OwnerId = userID
relation.TargetID = targetUser.ID
relation.Type = 1
if t := tx.Create(&relation); t.RowsAffected == 0 {
zap.S().Info("创建失败")
//事务回滚
tx.Rollback()
return -1, errors.New("创建好友记录失败")
}
relation = models.Relation{}
relation.OwnerId = TargetId
relation.TargetID = userID
relation.Type = 1
if t := tx.Create(&relation); t.RowsAffected == 0 {
zap.S().Info("创建失败")
//事务回滚
tx.Rollback()
return -1, errors.New("创建好友记录失败")
}
//提交事务
tx.Commit()
return 1, nil
}
通过昵称添加
通过昵称获取到用户id,然后对id进行查找添加
//AddFriendByName 昵称加好友
func AddFriendByName(userId uint, targetName string) (int, error) {
user, err := FindUserByName(targetName)
if err != nil {
return -1, errors.New("该用户不存在")
}
if user.ID == 0 {
zap.S().Info("未查询到用户")
return -1, errors.New("该用户不存在")
}
return AddFriend(userId, user.ID)
}
dao层完整代码:
package dao
import (
"HiChat/global"
"HiChat/models"
"errors"
"go.uber.org/zap"
)
//FriendList 获取好友列表
func FriendList(userId uint) (*[]models.UserBasic, error) {
relation := make([]models.Relation, 0)
if tx := global.DB.Where("owner_id = ? and type=1", userId).Find(&relation); tx.RowsAffected == 0 {
zap.S().Info("未查询到Relation数据")
return nil, errors.New("未查到好友关系")
}
userID := make([]uint, 0)
for _, v := range relation {
userID = append(userID, v.TargetID)
}
user := make([]models.UserBasic, 0)
if tx := global.DB.Where("id in ?", userID).Find(&user); tx.RowsAffected == 0 {
zap.S().Info("未查询到Relation好友关系")
return nil, errors.New("未查到好友")
}
return &user, nil
}
//AddFriendByName 昵称加好友
func AddFriendByName(userId uint, targetName string) (int, error) {
user, err := FindUserByName(targetName)
if err != nil {
return -1, errors.New("该用户不存在")
}
if user.ID == 0 {
zap.S().Info("未查询到用户")
return -1, errors.New("该用户不存在")
}
return AddFriend(userId, user.ID)
}
//AddFriend 加好友
func AddFriend(userID, TargetId uint) (int, error) {
if userID == TargetId {
return -2, errors.New("userID和TargetId相等")
}
//通过id查询用户
targetUser, err := FindUserID(TargetId)
if err != nil {
return -1, errors.New("未查询到用户")
}
if targetUser.ID == 0 {
zap.S().Info("未查询到用户")
return -1, errors.New("未查询到用户")
}
relation := models.Relation{}
if tx := global.DB.Where("owner_id = ? and target_id = ? and type = 1", userID, TargetId).First(&relation); tx.RowsAffected == 1 {
zap.S().Info("该好友存在")
return 0, errors.New("好友已经存在")
}
if tx := global.DB.Where("owner_id = ? and target_id = ? and type = 1", TargetId, userID).First(&relation); tx.RowsAffected == 1 {
zap.S().Info("该好友存在")
return 0, errors.New("好友已经存在")
}
//开启事务
tx := global.DB.Begin()
relation.OwnerId = userID
relation.TargetID = targetUser.ID
relation.Type = 1
if t := tx.Create(&relation); t.RowsAffected == 0 {
zap.S().Info("创建失败")
//事务回滚
tx.Rollback()
return -1, errors.New("创建好友记录失败")
}
relation = models.Relation{}
relation.OwnerId = TargetId
relation.TargetID = userID
relation.Type = 1
if t := tx.Create(&relation); t.RowsAffected == 0 {
zap.S().Info("创建失败")
//事务回滚
tx.Rollback()
return -1, errors.New("创建好友记录失败")
}
//提交事务
tx.Commit()
return 1, nil
}
返回结构统一
为了更统一的对api数据的返回,我们在common下新建resp.go
package common
import (
"encoding/json"
"fmt"
"net/http"
)
type H struct {
Code int
Msg string
Data interface{}
Rows interface{}
Total interface{}
}
func Resp(w http.ResponseWriter, code int, data interface{}, msg string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
h := H{
Code: code,
Data: data,
Msg: msg,
}
ret, err := json.Marshal(h)
if err != nil {
fmt.Println(err)
}
w.Write(ret)
}
func RespList(w http.ResponseWriter, code int, data interface{}, total interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
h := H{
Code: code,
Rows: data,
Total: total,
}
ret, err := json.Marshal(h)
if err != nil {
fmt.Println(err)
}
w.Write(ret)
}
func RespFail(w http.ResponseWriter, msg string) {
Resp(w, -1, nil, msg)
}
func RespOK(w http.ResponseWriter, data interface{}, msg string) {
Resp(w, 0, data, msg)
}
func RespOKList(w http.ResponseWriter, data interface{}, total interface{}) {
RespList(w, 0, data, total)
}
service层的api的实现
在service目录下新建relation.go文件
需要引入包:
import (
"strconv"
"HiChat/common"
"HiChat/dao"
"HiChat/models"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
好友列表
//user对返回数据进行屏蔽
type user struct {
Name string
Avatar string
Gender string
Phone string
Email string
Identity string
}
func FriendList(ctx *gin.Context) {
id, _ := strconv.Atoi(ctx.Request.FormValue("userId"))
users, err := dao.FriendList(uint(id))
if err != nil {
zap.S().Info("获取好友列表失败", err)
ctx.JSON(200, gin.H{
"code": -1, // 0成功 -1失败
"message": "好友为空",
})
return
}
infos := make([]user, 0)
for _, v := range *users {
info := user{
Name: v.Name,
Avatar: v.Avatar,
Gender: v.Gender,
Phone: v.Phone,
Email: v.Email,
Identity: v.Identity,
}
infos = append(infos, info)
}
common.RespOKList(ctx.Writer, infos, len(infos))
}
添加好友
//AddFriendByName 通过昵称加好友
func AddFriendByName(ctx *gin.Context) {
user := ctx.PostForm("userId")
userId, err := strconv.Atoi(user)
if err != nil {
zap.S().Info("类型转换失败", err)
return
}
tar := ctx.PostForm("targetName")
target, err := strconv.Atoi(tar)
if err != nil {
code, err := dao.AddFriendByName(uint(userId), tar)
if err != nil {
HandleErr(code, ctx, err)
return
}
} else {
code, err := dao.AddFriend(uint(userId), uint(target))
if err != nil {
HandleErr(code, ctx, err)
return
}
}
ctx.JSON(200, gin.H{
"code": 0, // 0成功 -1失败
"message": "添加好友成功",
})
}
func HandleErr(code int, ctx *gin.Context, err error) {
switch code {
case -1:
ctx.JSON(200, gin.H{
"code": -1, // 0成功 -1失败
"message": err.Error(),
})
case 0:
ctx.JSON(200, gin.H{
"code": -1, // 0成功 -1失败
"message": "该好友已经存在",
})
case -2:
ctx.JSON(200, gin.H{
"code": -1, // 0成功 -1失败
"message": "不能添加自己",
})
}
}
配置路由
//好友关系
relation := v1.Group("relation").Use(middlewear.JWY())
{
relation.POST("/list", service.FriendList)
relation.POST("/add", service.AddFriendByName)
}
测试
好友列表
由于是post请求,使用postman就行测试
添加好友
好友列表:
总结
本篇文章我们介绍了好友关系如何设计,进一步提升事物的抽象能力,并且简单介绍了什么是事务,最后我们完成了好友列表和添加好友的功能,后续我们将介绍群关系设计,以及群列表、建群,加群等功能的实现。