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

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 检索

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

表结构示例如下:

CREATETABLE`friend`(`id`bigintNOTNULLAUTO_INCREMENT,`user_id`varchar(64)NOTNULLCOMMENT'用户ID',`friend_id`varchar(64)NOTNULLCOMMENT'好友ID',`status`tinyint(1)NOTNULLDEFAULT'1'COMMENT'1:正常',`create_time`datetimeNOTNULLDEFAULTCURRENT_TIMESTAMP,PRIMARYKEY(`id`),KEY`idx_user_id`(`user_id`),KEY`idx_friend_id`(`friend_id`))ENGINE=InnoDBDEFAULTCHARSET=utf8mb4COMMENT='好友关系表';

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

packageconfigimport("github.com/zeromicro/go-zero/core/stores/cache""github.com/zeromicro/go-zero/zrpc")typeConfigstruct{zrpc.RpcServerConf MySQLstruct{DataSourcestring}CacheRedis cache.CacheConf}

etc/social.yaml示例:

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

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

packagesvcimport("im-system/apps/social/rpc/internal/config""im-system/apps/social/rpc/model""github.com/zeromicro/go-zero/core/stores/sqlx")typeServiceContextstruct{Config config.Config FriendModel model.FriendModel FriendReqModel model.FriendRequestModel GroupModel model.GroupModel GroupMemberModel model.GroupMemberModel GroupReqModel model.GroupRequestModel}funcNewServiceContext(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-srcdeploy/sql/social.sql-dirapps/social/rpc/model-prefixsocial_-stylegoZero

扩展模型方法

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

1 ) 检查好友关系:FindOneByUserIdAndFriendId

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

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

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

3 ) 批量新增好友记录

// friendmodel_ext.gofunc(m defaultFriendModel)InsertBatch(ctx context.Context,conn sqlx.Session,list[]Friend)error{iflen(list)==0{returnnil}varbuf strings.Builder buf.WriteString(fmt.Sprintf("insert into %s (%s) values ",m.table,friendRows))args:=make([]interface{},0,len(list)*len(friendFieldNames))fori,item:=rangelist{ifi>0{buf.WriteString(", ")}buf.WriteString("(?, ?, ?, ?)")args=append(args,item.UserId,item.FriendId,item.Status,item.CreateTime)}_,err:=conn.ExecCtx(ctx,buf.String(),args...)returnerr}

4 ) 事务支持

模型接口中增加 Transact 方法:

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

其他模型同理。

业务逻辑实现

好友申请

申请流程:

  1. 检查目标用户与自己是否已经是好友。
  2. 检查是否存在未处理的申请记录。
  3. 创建新的申请记录。
// friendapplylogic.gofunc(l FriendApplyLogic)FriendApply(in social.FriendApplyRequest)(*social.FriendApplyResponse,error){// 1. 检查是否已是好友_,err:=l.svcCtx.FriendModel.FindOneByUserIdAndFriendId(l.ctx,in.UserId,in.FriendId)iferr==nil{returnnil,errx.NewCodeError(errx.CodeAlreadyFriend,"已为好友关系")}if!errors.Is(err,sqlx.ErrNotFound){returnnil,errx.NewCodeErrorf(errx.CodeDBError,"查询好友关系失败: %v",err)}// 2. 检查是否有未处理的申请_,err=l.svcCtx.FriendReqModel.FindByUserIdAndFriendId(l.ctx,in.UserId,in.FriendId)iferr==nil{returnnil,errx.NewCodeError(errx.CodeRequestExists,"已存在待处理的申请")}if!errors.Is(err,sqlx.ErrNotFound){returnnil,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)iferr!=nil{returnnil,errx.NewCodeErrorf(errx.CodeDBError,"创建好友申请失败: %v",err)}return&social.FriendApplyResponse{},nil}

好友申请处理

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

// friendapplyhandlelogic.gofunc(l FriendApplyHandleLogic)FriendApplyHandle(in social.FriendApplyHandleRequest)(*social.FriendApplyHandleResponse,error){// 1. 获取申请记录applyId:=in.ApplyId apply,err:=l.svcCtx.FriendReqModel.FindOne(l.ctx,applyId)iferr!=nil{returnnil,errx.NewCodeError(errx.CodeApplyNotFound,"申请记录不存在")}// 2. 检查状态ifapply.Status!=1{returnnil,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)iferr!=nil{returnerr}// 如果通过,建立两条好友关系ifin.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)iferr!=nil{returnerr}}returnnil})iferr!=nil{returnnil,errx.NewCodeErrorf(errx.CodeDBError,"处理好友申请失败: %v",err)}return&social.FriendApplyHandleResponse{},nil}

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

好友列表查询

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

注册拦截器

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

funcmain(){flag.Parse()varc 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))defers.Stop()fmt.Printf("Starting social rpc server at %s...\n",c.ListenOn)s.Start()}

测试验证

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

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

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

总结

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

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

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