[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就行测试

添加好友

好友列表:

总结

本篇文章我们介绍了好友关系如何设计,进一步提升事物的抽象能力,并且简单介绍了什么是事务,最后我们完成了好友列表和添加好友的功能,后续我们将介绍群关系设计,以及群列表、建群,加群等功能的实现。