纲要
- 用户详情查询
- 基于
GetUserInfo接口实现 - 使用模型
FindOne按主键查询
- 基于
- 用户搜索功能
- 按手机号精确查询(复用
FindOneByPhone) - 按用户名模糊查询(新增
FindByName) - 按 ID 集合批量查询(新增
FindByIds)
- 按手机号精确查询(复用
- 统一错误处理体系
- 自定义错误码与描述映射
- 基于
go-zero的errx包创建标准错误对象 - RPC 拦截器实现错误日志记录与客户端友好响应
- 在业务逻辑中替换原始错误,应用统一错误码
用户详情查询
在user.proto中已定义GetUserInfo方法,其请求参数为uid,响应为用户模型。业务逻辑非常简单:根据uid调用模型层的FindOne方法,若用户不存在则返回“用户不存在”错误。
在apps/user/rpc/internal/logic/getuserinfologic.go中实现:
packagelogicimport("context""errors""im-system/apps/user/rpc/internal/svc""im-system/apps/user/rpc/user""im-system/common/model""github.com/zeromicro/go-zero/core/stores/sqlx""github.com/zeromicro/go-zero/core/logx")typeGetUserInfoLogicstruct{ctx context.Context svcCtx*svc.ServiceContext logx.Logger}funcNewGetUserInfoLogic(ctx context.Context,svcCtx*svc.ServiceContext)*GetUserInfoLogic{return&GetUserInfoLogic{ctx:ctx,svcCtx:svcCtx,Logger:logx.WithContext(ctx),}}varErrUserNotFound=errors.New("用户不存在")func(l*GetUserInfoLogic)GetUserInfo(in*user.GetUserInfoRequest)(*user.GetUserInfoResponse,error){u,err:=l.svcCtx.UserModel.FindOne(l.ctx,in.Uid)iferr!=nil{iferrors.Is(err,sqlx.ErrNotFound){returnnil,ErrUserNotFound}returnnil,err}return&user.GetUserInfoResponse{Uid:u.Uid,Username:u.Username,Phone:u.Phone,Avatar:u.Avatar,Gender:int64(u.Gender),},nil}用户搜索功能
搜索接口FindUser支持三种方式:手机号精确查询、用户名模糊查询、ID 集合批量查询。模型层需要提供对应的方法。
模型扩展
在common/model/usermodel_ext.go中新增方法:
packagemodelimport("context""fmt""strings""github.com/zeromicro/go-zero/core/stores/sqlx")// FindByName 按用户名模糊查询func(m*defaultUserModel)FindByName(ctx context.Context,namestring)([]*User,error){query:=fmt.Sprintf("select %s from %s where username like ? limit 20",userRows,m.table)varresp[]*User err:=m.conn.QueryRowsCtx(ctx,&resp,query,"%"+name+"%")returnresp,err}// FindByIds 按ID集合批量查询func(m*defaultUserModel)FindByIds(ctx context.Context,ids[]string)([]*User,error){iflen(ids)==0{returnnil,nil}placeholders:=strings.Repeat("?,",len(ids))placeholders=placeholders[:len(placeholders)-1]query:=fmt.Sprintf("select %s from %s where uid in (%s)",userRows,m.table,placeholders)args:=make([]interface{},len(ids))fori,id:=rangeids{args[i]=id}varresp[]*User err:=m.conn.QueryRowsCtx(ctx,&resp,query,args...)returnresp,err}注意:userRows为生成代码中定义的字段列表常量,直接使用即可。
业务逻辑实现
在finduserlogic.go中,根据请求参数的不同,路由到对应的查询方法。
packagelogicimport("context""im-system/apps/user/rpc/internal/svc""im-system/apps/user/rpc/user""im-system/common/model""github.com/zeromicro/go-zero/core/logx")typeFindUserLogicstruct{ctx context.Context svcCtx*svc.ServiceContext logx.Logger}funcNewFindUserLogic(ctx context.Context,svcCtx*svc.ServiceContext)*FindUserLogic{return&FindUserLogic{ctx:ctx,svcCtx:svcCtx,Logger:logx.WithContext(ctx),}}func(l*FindUserLogic)FindUser(in*user.FindUserRequest)(*user.FindUserResponse,error){varusers[]*model.Uservarerrerrorswitch{casein.Phone!="":// 手机号精确查询u,e:=l.svcCtx.UserModel.FindOneByPhone(l.ctx,in.Phone)ife==nil{users=append(users,u)}err=ecasein.Username!="":// 用户名模糊查询users,err=l.svcCtx.UserModel.FindByName(l.ctx,in.Username)caselen(in.Ids)>0:// ID集合查询users,err=l.svcCtx.UserModel.FindByIds(l.ctx,in.Ids)default:returnnil,errors.New("搜索条件不能为空")}iferr!=nil{returnnil,err}// 转换为响应结构体resp:=&user.FindUserResponse{}for_,u:=rangeusers{resp.Users=append(resp.Users,&user.GetUserInfoResponse{Uid:u.Uid,Username:u.Username,Phone:u.Phone,Avatar:u.Avatar,Gender:int64(u.Gender),})}returnresp,nil}统一错误处理设计
随着服务增多,业务错误若直接返回原始error,既不利于日志追踪,也不便于客户端统一解析。我们需要构建一套标准错误体系:包含错误码、描述、日志记录,并通过 RPC 拦截器自动处理。
错误码定义
在common/errx包中定义错误码与描述映射。
packageerrxconst(// 通用错误码,前三位为业务标识,后三位为具体错误CodeServerError=100001CodeParamInvalid=100002CodeDBError=100003CodeUserNotFound=100004CodeUserExists=100005CodePasswordWrong=100006CodeTokenGenerateErr=100007)varcodeMsg=map[int]string{CodeServerError:"服务器内部错误",CodeParamInvalid:"请求参数有误",CodeDBError:"数据库繁忙,请稍后再试",CodeUserNotFound:"用户不存在",CodeUserExists:"手机号已注册",CodePasswordWrong:"密码错误",CodeTokenGenerateErr:"令牌生成失败",}funcGetMsg(codeint)string{ifmsg,ok:=codeMsg[code];ok{returnmsg}returncodeMsg[CodeServerError]}自定义错误对象
利用go-zero提供的github.com/zeromicro/go-zero/core/errorx包(或errx),创建带有错误码的错误类型。在common/errx中继续封装。
packageerrximport("github.com/zeromicro/go-zero/core/errorx")// NewCodeError 创建带错误码的错误funcNewCodeError(codeint)error{returnerrorx.NewCodeError(code,GetMsg(code))}// NewCodeErrorf 创建带错误码和自定义描述的错误(描述用于日志,错误码对应信息返回给客户端)funcNewCodeErrorf(codeint,formatstring,args...interface{})error{returnerrorx.NewCodeErrorf(code,GetMsg(code),format,args...)}// 快捷错误方法funcDBError()error{returnNewCodeError(CodeDBError)}funcUserNotFound()error{returnNewCodeError(CodeUserNotFound)}funcUserExists()error{returnNewCodeError(CodeUserExists)}funcPasswordWrong()error{returnNewCodeError(CodePasswordWrong)}funcTokenGenerateErr()error{returnNewCodeError(CodeTokenGenerateErr)}业务逻辑中使用统一错误
以登录逻辑为例,将所有直接返回errors.New的地方替换为errx中的错误。
func(l*LoginLogic)Login(in*user.LoginRequest)(*user.LoginResponse,error){u,err:=l.svcCtx.UserModel.FindOneByPhone(l.ctx,in.Phone)iferr!=nil{iferrors.Is(err,sqlx.ErrNotFound){returnnil,errx.UserNotFound()// 使用统一错误}returnnil,errx.NewCodeErrorf(errx.CodeDBError,"根据手机号查询用户失败: %v",err)}if!crypt.CheckPassword(u.Password,in.Password){returnnil,errx.PasswordWrong()}expire:=time.Duration(l.svcCtx.Config.JWT.ExpireInSeconds)*time.Second tokenStr,err:=jwt.GenerateToken(u.Uid,[]byte(l.svcCtx.Config.JWT.Secret),expire)iferr!=nil{returnnil,errx.NewCodeErrorf(errx.CodeTokenGenerateErr,"生成token失败: %v",err)}return&user.LoginResponse{Uid:u.Uid,Token:tokenStr,},nil}RPC 拦截器统一处理
为将错误码信息写入 gRPC 响应的status中,同时将详细错误记录到日志,我们编写一个 gRPC 拦截器。
在common/interceptor/rpc_err_interceptor.go中:
packageinterceptorimport("context""im-system/common/errx""github.com/zeromicro/go-zero/core/errorx""github.com/zeromicro/go-zero/core/logx""google.golang.org/grpc""google.golang.org/grpc/codes""google.golang.org/grpc/status")// RpcErrInterceptor 服务端统一错误拦截器funcRpcErrInterceptor(ctx context.Context,reqinterface{},info*grpc.UnaryServerInfo,handler grpc.UnaryHandler)(interface{},error){resp,err:=handler(ctx,req)iferr==nil{returnresp,nil}// 记录日志logx.WithContext(ctx).Errorf("rpc err: %v",err)// 尝试提取自定义错误码ifcodeErr:=errorx.CodeErrorFrom(err);codeErr!=nil{// 构造 gRPC 状态,将错误码和消息返回给客户端returnnil,status.Errorf(codes.Code(codeErr.Code),codeErr.Error())}// 非自定义错误,统一返回内部错误returnnil,status.Errorf(codes.Internal,errx.GetMsg(errx.CodeServerError))}然后在服务启动时注册拦截器,在 apps/user/rpc/user.go 或对应的 main 函数中:
packagemainimport("flag""fmt""im-system/apps/user/rpc/internal/config""im-system/apps/user/rpc/internal/server""im-system/apps/user/rpc/internal/svc""im-system/apps/user/rpc/user""im-system/common/interceptor""github.com/zeromicro/go-zero/core/conf""github.com/zeromicro/go-zero/zrpc""google.golang.org/grpc")varconfigFile=flag.String("f","etc/user.yaml","the config file")funcmain(){flag.Parse()varc config.Config conf.MustLoad(*configFile,&c)s:=zrpc.MustNewServer(c.RpcServerConf,func(grpcServer*grpc.Server){user.RegisterUserServer(grpcServer,server.NewUserServer(svc.NewServiceContext(c)))},zrpc.WithUnaryServerInterceptor(interceptor.RpcErrInterceptor))defers.Stop()fmt.Printf("Starting rpc server at %s...\n",c.ListenOn)s.Start()}这一段是否有问题啊?
错误处理流程
拦截器捕获所有从handler返回的错误,记录完整错误堆栈到日志,同时向客户端返回仅包含错误码和友好描述的 gRPC 状态,既方便运维排查,又避免敏感信息泄露。
测试验证
完成上述编码后,启动用户服务,使用grpcurl或单元测试验证:
- 查询存在的用户,返回正确信息。
- 查询不存在的用户,返回 gRPC 状态码为
NotFound或自定义错误码,消息为“用户不存在”。 - 搜索功能:分别测试手机号、用户名、ID 集合三种情况,确认结果正确。
- 登录错误:输入错误密码,返回“密码错误”。
总结
至此,用户服务的搜索、详情查询及统一错误处理体系已构建完成。这套错误处理模式可快速复制到社交服务、IM 服务等其他微服务中,确保整个系统的可维护性与用户体验一致性。