Go-Zero项目开发7: 构建社交服务RPC与好友/群业务实现

纲要

  • 社交服务业务需求
    • 好友关系管理(申请、处理、好友列表)
    • 群组管理(创建、申请、加入、退群等)
  • 数据表设计
    • 好友关系表 friend 的冗余存储方案
    • 好友申请表 friend_request
    • 群信息表、群成员表、群申请表
  • 项目结构更新
    • apps/social/rpc 目录及关键文件
  • 构建社交 RPC 服务
    • 编写 social.proto 定义服务接口
    • 使用 goctl rpc 生成代码
    • 配置 etc/social.yaml 及依赖注入
  • 模型生成与扩展
    • 基于 SQL 生成 5 张表的模型
    • 解决包名冲突的前缀设计
    • 扩展模型方法:好友关系检查、申请查询、批量新增好友记录
  • 业务逻辑实现
    • 好友申请:重复检查、已有关系校验、新增申请记录
    • 好友申请处理:状态校核、事务内修改状态并建立好友关系
    • 好友列表查询
    • 事务封装:模型层支持 Transact 方法
  • 测试验证
    • 通过 gRPC 客户端进行接口测试

社交服务业务需求

在即时通讯系统中,社交模块负责管理用户之间的关系网络,核心功能包括:

  • 好友管理:申请好友、处理申请、好友列表查询。
  • 群组管理:创建群组、申请加群、处理加群申请、群成员列表、退群。

本文重点围绕好友关系的设计及核心业务实现,群组功能的实现模式与之类似,可在后续迭代中补充。

数据表设计

社交服务涉及 5 张数据表:好友关系表 friend、好友申请表 friend_request、群信息表 group、群成员表 group_member、群申请表 group_request

好友关系表设计是一个关键点,常见两种方案:

方案存储方式优点缺点
冗余存储双方分别记录一条 user_id → friend_id 数据查询简单,性能高数据冗余,占用双倍存储
唯一 Key根据双方 ID 排序生成唯一 Key,仅存储一条节省空间查询复杂,需分别按双方 ID 检索

本系统采用冗余存储方案,因其查询效率高,且好友关系数据总量相对可控。

表结构示例如下:

CREATE TABLE `friend` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `user_id` varchar(64) NOT NULL COMMENT '用户ID',
  `friend_id` varchar(64) NOT NULL COMMENT '好友ID',
  `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '1:正常',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `idx_user_id` (`user_id`),
  KEY `idx_friend_id` (`friend_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='好友关系表';

friend_request 表类似,增加请求状态字段。

项目结构更新

apps/social/rpc 下组织社交服务代码:

apps/social/rpc/
├── internal/
│   ├── config/
│   ├── server/
│   ├── svc/
│   └── logic/
├── model/                    # 生成的模型代码(带前缀)
├── social.proto 
└── social.go 

构建社交 RPC 服务

编写 Proto 文件

在 social.proto 中先定义好友相关 RPC:

syntax = "proto3";
 
package social;
 
option go_package = "./social";
 
message FriendApplyRequest {
  string user_id = 1;
  string friend_id = 2;  // 目标用户ID 
  string reason = 3;
}
 
message FriendApplyResponse {}
 
message FriendApplyHandleRequest {
  int64 apply_id = 1;
  int32 handle_type = 2;  // 1:通过 2:拒绝 
}
 
message FriendApplyHandleResponse {}
 
message FriendListRequest {
  string user_id = 1;
}
 
message FriendListResponse {
  repeated string friend_ids = 1;
}
 
service Social {
  // 申请好友 
  rpc FriendApply(FriendApplyRequest) returns (FriendApplyResponse);
  // 处理好友申请 
  rpc FriendApplyHandle(FriendApplyHandleRequest) returns (FriendApplyHandleResponse);
  // 好友列表 
  rpc FriendList(FriendListRequest) returns (FriendListResponse);
}

生成代码

执行命令:

$ goctl rpc protoc apps/social/rpc/social.proto --goout=apps/social/rpc/ --go-grpcout=apps/social/rpc/ --zrpc_out=apps/social/rpc/

生成后的 social.go 为服务入口,internal 下包含通用组件。

配置与依赖注入

internal/config/config.go

package config 
 
import (
	"github.com/zeromicro/go-zero/core/stores/cache"
	"github.com/zeromicro/go-zero/zrpc"
)
 
type Config struct {
	zrpc.RpcServerConf 
	MySQL struct {
		DataSource string 
	}
	CacheRedis cache.CacheConf 
}

etc/social.yaml 示例:

Name: social.rpc 
ListenOn: 0.0.0.0:10002 
Etcd:
  Hosts:
    - 192.168.1.10:2379 
  Key: social.rpc 
MySQL:
  DataSource: root:123456@tcp(192.168.1.10:3306)/im_db?charset=utf8mb4&parseTime=True 
CacheRedis:
  - Host: 192.168.1.10:6379 
    Pass: "yourpass"

internal/svc/servicecontext.go 中注入所有模型:

package svc 
 
import (
	"im-system/apps/social/rpc/internal/config"
	"im-system/apps/social/rpc/model"
	"github.com/zeromicro/go-zero/core/stores/sqlx"
)
 
type ServiceContext struct {
	Config           config.Config 
	FriendModel      model.FriendModel 
	FriendReqModel   model.FriendRequestModel 
	GroupModel       model.GroupModel 
	GroupMemberModel model.GroupMemberModel 
	GroupReqModel    model.GroupRequestModel 
}
 
func NewServiceContext(c config.Config) *ServiceContext {
	conn := sqlx.NewMysql(c.MySQL.DataSource)
	// 注意:生成模型时指定了前缀,因此创建的模型类型名会有前缀 
	return &ServiceContext{
		Config:           c,
		FriendModel:      model.NewFriendModel(conn, c.CacheRedis),
		FriendReqModel:   model.NewFriendRequestModel(conn, c.CacheRedis),
		// ... 其他模型 
	}
}

模型生成与扩展

生成数据模型

使用 goctl model 基于 SQL 生成模型,通过 -prefix 参数添加前缀,避免多个服务模型命名冲突:

$ goctl model mysql ddl -src deploy/sql/social.sql -dir apps/social/rpc/model -prefix social_ -style goZero 

扩展模型方法

为满足业务需求,在模型层添加自定义方法。

1 ) 检查好友关系:FindOneByUserIdAndFriendId

// 在 friendmodel_ext.go 中 
func (m defaultFriendModel) FindOneByUserIdAndFriendId(ctx context.Context, uid, fuid string) (Friend, error) {
	query := fmt.Sprintf("select %s from %s where userid = ? and friendid = ? limit 1", friendRows, m.table)
	var resp Friend 
	err := m.conn.QueryRowCtx(ctx, &resp, query, uid, fuid)
	if err != nil {
		return nil, err 
	}
	return &resp, nil 
}

2 ) 查询好友申请记录:FindByUserIdAndFriendId

// friendrequestmodel_ext.go 
func (m defaultFriendRequestModel) FindByUserIdAndFriendId(ctx context.Context, reqUid, targetUid string) (FriendRequest, error) {
	query := fmt.Sprintf("select %s from %s where userid = ? and friendid = ? and status = 1 limit 1", friendRequestRows, m.table)
	var resp FriendRequest 
	err := m.conn.QueryRowCtx(ctx, &resp, query, reqUid, targetUid)
	if err != nil {
		return nil, err 
	}
	return &resp, nil 
}

3 ) 批量新增好友记录

// friendmodel_ext.go 
func (m defaultFriendModel) InsertBatch(ctx context.Context, conn sqlx.Session, list []Friend) error {
	if len(list) == 0 {
		return nil 
	}
	var buf strings.Builder 
	buf.WriteString(fmt.Sprintf("insert into %s (%s) values ", m.table, friendRows))
	args := make([]interface{}, 0, len(list)*len(friendFieldNames))
	for i, item := range list {
		if i > 0 {
			buf.WriteString(", ")
		}
		buf.WriteString("(?, ?, ?, ?)")
		args = append(args, item.UserId, item.FriendId, item.Status, item.CreateTime)
	}
	_, err := conn.ExecCtx(ctx, buf.String(), args...)
	return err 
}

4 ) 事务支持

模型接口中增加 Transact 方法:

// friendmodel.go 接口定义 
FriendModel interface {
	// ... 其他方法 
	Transact(ctx context.Context, fn func(ctx context.Context, session sqlx.Session) error) error 
}
 
// 实现 
func (m *defaultFriendModel) Transact(ctx context.Context, fn func(ctx context.Context, session sqlx.Session) error) error {
	return m.conn.Transact(ctx, fn)
}

其他模型同理。

业务逻辑实现

好友申请

申请流程:

  1. 检查目标用户与自己是否已经是好友。
  2. 检查是否存在未处理的申请记录。
  3. 创建新的申请记录。
// friendapplylogic.go 
func (l FriendApplyLogic) FriendApply(in social.FriendApplyRequest) (*social.FriendApplyResponse, error) {
	// 1. 检查是否已是好友 
	_, err := l.svcCtx.FriendModel.FindOneByUserIdAndFriendId(l.ctx, in.UserId, in.FriendId)
	if err == nil {
		return nil, errx.NewCodeError(errx.CodeAlreadyFriend, "已为好友关系")
	}
	if !errors.Is(err, sqlx.ErrNotFound) {
		return nil, errx.NewCodeErrorf(errx.CodeDBError, "查询好友关系失败: %v", err)
	}
 
	// 2. 检查是否有未处理的申请 
	_, err = l.svcCtx.FriendReqModel.FindByUserIdAndFriendId(l.ctx, in.UserId, in.FriendId)
	if err == nil {
		return nil, errx.NewCodeError(errx.CodeRequestExists, "已存在待处理的申请")
	}
	if !errors.Is(err, sqlx.ErrNotFound) {
		return nil, errx.NewCodeErrorf(errx.CodeDBError, "查询申请记录失败: %v", err)
	}
 
	// 3. 新增申请 
	newReq := &model.FriendRequest{
		UserId:     in.UserId,
		FriendId:   in.FriendId,
		Reason:     in.Reason,
		Status:     1, // 未处理 
		CreateTime: time.Now(),
	}
	_, err = l.svcCtx.FriendReqModel.Insert(l.ctx, newReq)
	if err != nil {
		return nil, errx.NewCodeErrorf(errx.CodeDBError, "创建好友申请失败: %v", err)
	}
 
	return &social.FriendApplyResponse{}, nil 
}

好友申请处理

处理流程依赖事务,确保状态修改和好友关系建立原子执行。

// friendapplyhandlelogic.go 
func (l FriendApplyHandleLogic) FriendApplyHandle(in social.FriendApplyHandleRequest) (*social.FriendApplyHandleResponse, error) {
	// 1. 获取申请记录 
	applyId := in.ApplyId 
	apply, err := l.svcCtx.FriendReqModel.FindOne(l.ctx, applyId)
	if err != nil {
		return nil, errx.NewCodeError(errx.CodeApplyNotFound, "申请记录不存在")
	}
 
	// 2. 检查状态 
	if apply.Status != 1 {
		return nil, errx.NewCodeError(errx.CodeApplyAlreadyProcessed, "申请已处理")
	}
 
	// 3. 事务处理 
	err = l.svcCtx.FriendReqModel.Transact(l.ctx, func(ctx context.Context, session sqlx.Session) error {
		// 更新申请状态 
		apply.Status = in.HandleType 
		err := l.svcCtx.FriendReqModel.UpdateWithSession(ctx, session, apply)
		if err != nil {
			return err 
		}
 
		// 如果通过,建立两条好友关系 
		if in.HandleType == 1 {
			friends := []*model.Friend{
				{UserId: apply.UserId, FriendId: apply.FriendId, Status: 1, CreateTime: time.Now()},
				{UserId: apply.FriendId, FriendId: apply.UserId, Status: 1, CreateTime: time.Now()},
			}
			err = l.svcCtx.FriendModel.InsertBatch(ctx, session, friends)
			if err != nil {
				return err 
			}
		}
		return nil 
	})
	if err != nil {
		return nil, errx.NewCodeErrorf(errx.CodeDBError, "处理好友申请失败: %v", err)
	}
 
	return &social.FriendApplyHandleResponse{}, nil 
}

注意:UpdateWithSession 需在模型层提供使用 session 的方法,以便在事务内执行。

好友列表查询

// friendlistlogic.go 
func (l FriendListLogic) FriendList(in social.FriendListRequest) (*social.FriendListResponse, error) {
	friends, err := l.svcCtx.FriendModel.FindByUserId(ctx, in.UserId)
	if err != nil {
		return nil, errx.NewCodeErrorf(errx.CodeDBError, "查询好友列表失败: %v", err)
	}
 
	friendIds := make([]string, 0, len(friends))
	for _, f := range friends {
		friendIds = append(friendIds, f.FriendId)
	}
	return &social.FriendListResponse{FriendIds: friendIds}, nil 
}

注册拦截器

social.go 入口处注册错误拦截器,采用与用户服务相同的模式:

func main() {
	flag.Parse()
	var c config.Config 
	conf.MustLoad(*configFile, &c)
 
	s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) {
		social.RegisterSocialServer(grpcServer, server.NewSocialServer(svc.NewServiceContext(c)))
	}, zrpc.WithUnaryServerInterceptor(interceptor.RpcErrInterceptor))
	defer s.Stop()
 
	fmt.Printf("Starting social rpc server at %s...\n", c.ListenOn)
	s.Start()
}

测试验证

使用 grpcurl 或单元测试验证好友功能:

  • 申请好友:传入双方 ID,第一次应成功;重复申请应返回“已存在待处理申请”;若已为好友则提示“已为好友”。
  • 处理申请:传入申请 ID 和操作类型,成功后好友关系表中出现两条记录。
  • 列表查询:传入用户 ID,返回好友 ID 列表。

通过事务保障,如果插入好友关系失败,申请状态不会改变,有效防止数据不一致。

总结

本文完成了社交服务中好友功能的完整搭建,涵盖了:

  • 冗余存储的好友关系设计决策。
  • 利用 goctl 快速生成 RPC 与模型代码。
  • 模型层自定义方法的扩展,包括支持事务的批量插入。
  • 业务逻辑中对状态机、并发安全的合理处理。
  • 统一错误码和拦截器的应用。

群组相关的创建、申请、成员管理等业务可参照好友模式实现。下一节我们将整合社交 API 层,对外提供 HTTP 接口,逐步构建完整的社交服务。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Wang's Blog

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值