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)}其他模型同理。
业务逻辑实现
好友申请
申请流程:
- 检查目标用户与自己是否已经是好友。
- 检查是否存在未处理的申请记录。
- 创建新的申请记录。
// 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 接口,逐步构建完整的社交服务。
