纲要
- 社交服务业务需求
- 好友关系管理(申请、处理、好友列表)
- 群组管理(创建、申请、加入、退群等)
- 数据表设计
- 好友关系表
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)
}
其他模型同理。
业务逻辑实现
好友申请
申请流程:
- 检查目标用户与自己是否已经是好友。
- 检查是否存在未处理的申请记录。
- 创建新的申请记录。
// 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 接口,逐步构建完整的社交服务。


被折叠的 条评论
为什么被折叠?



