gozero-商城之用户微服务构建

本文:是从我博客园的文章搬过来,我博客园的地址为:https://www.cnblogs.com/lisus2000

一:商城微服务简介

该商城主要包括的微服务有:购物车、首页、订单服务、支付服务、用户服务、商品服务,主要采用的是go-zero来实现

商城的思维导图如下

 

 图片转自:https://bbs.youkuaiyun.com/topics/608056514

从以上思维导图可以看出,我们根据业务职能做如下微服务的划分:

  商品服务(product) - 商品的添加、信息查询、库存管理等功能
  购物车服务(cart) - 购物车的增删改查、收藏等功能
  订单服务(order) - 生成订单,订单管理等功能
  支付服务(pay) - 通过调用第三方支付实现支付等功能
  账号服务(user) - 用户信息、实名认证、账号设置、地址管理等功能
  首页服务(home) - 首页商品推荐、排行榜、限时开抢、banner等功能
  每个服务都可以再分为 api 服务和 rpc 服务。

  api 服务对外,可提供给 app 调用。

  rpc 服务是对内的,可提供给内部 api 服务或者其他 rpc 服务调用。
整个项目的流程大致如下:

  

 图片转自:https://bbs.youkuaiyun.com/topics/608056514

二:项目目录结构创建

二用户表设计及Model开发

CREATE TABLE `user` (
    `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '用户ID',
    `userIdentity` varchar(255) DEFAULT '' COMMENT '用户唯一标识',
    `userName` varchar(50) NOT NULL DEFAULT '' COMMENT '用户名',
    `passWord` varchar(50) NOT NULL DEFAULT '' COMMENT '用户密码,MD5加密',
    `userNick` varchar(100) DEFAULT '' COMMENT '用户昵称',
    `userFace` varchar(255) DEFAULT '' COMMENT '用户头像地址',
    `UserSex` tinyint(1) DEFAULT '0' COMMENT '用户性别:0男,1女,2保密',
    `userEmail` varchar(255) DEFAULT '' COMMENT '用户邮箱',
    `userPhone` varchar(20) NOT NULL DEFAULT '' COMMENT '手机号',
    `loginAddress` varchar(255) DEFAULT '' COMMENT '用户登录IP地址',
    `create_time` timestamp default CURRENT_TIMESTAMP not null comment '创建时间',
    `update_time` timestamp default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
    PRIMARY KEY (`id`),
    UNIQUE KEY `userName` (`userName`),
    UNIQUE KEY `userPhone` (`userPhone`),
    KEY `updateTime` (`updateTime`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
用户MODEL生成
把目录切换到user/model目录下,然后在终端运行如下命令
goctl model mysql ddl -src user.sql -d -dir .

运行完后在model目录会看到如下内容

 

注:在数据库中先创建好表 

三:用户RPC接口生成

1.编写proto文件
在rpc目录下建立account.proto文件
syntax = "proto3";

package account;

option go_package = "./account";

message RegisterReq{
  string UserName = 1;
  string PassWord = 2;
  string UserNick = 3;
  string UserFace = 4;
  int64 UserSex = 5;
  string UserEmail = 6;
}

message LoginReq{
  string UserName = 1;
  string PassWord = 2;
}

message CommonResply{
  int64 Code = 1;
  string Message = 2;
  string Data = 3;
}

message UserInfoReq{
  string UserIdentity = 1;
}

service user{
  rpc Register(RegisterReq) returns(CommonResply);
  rpc Login(LoginReq) returns(CommonResply);
  rpc UserInfo(UserInfoReq) returns (CommonResply);
}
这时候在这个目录下运行这个命令
goctl rpc protoc account.proto --go_out=./types --go-grpc_out=./types --zrpc_out=. -style go-zero

 就会生成相应的RPC 文件,生成后的目录如下

 2.rpc配置

 (1)修改rpc/etc下的account.yml增加相应的配置

 (2)在internal目录下的config.go建立相应的配置

 (3)在internal目录下的loginc目录编写相应接口的逻辑

登录接口的代码如下:

  

 
package logic

import (
   "context"
   "encoding/json"
   "errors"
   "microshop/comm"
   "microshop/user/model"
   "net/http"

   "microshop/user/rpc/internal/svc"
   "microshop/user/rpc/types/account"

   "github.com/zeromicro/go-zero/core/logx"
)

type LoginLogic struct {
   ctx    context.Context
   svcCtx *svc.ServiceContext
   logx.Logger
}

func NewLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LoginLogic {
   return &LoginLogic{
      ctx:    ctx,
      svcCtx: svcCtx,
      Logger: logx.WithContext(ctx),
   }
}

func (l *LoginLogic) Login(in *account.LoginReq) (*account.CommonResply, error) {
   // todo: add your logic here and delete this line
   //获取用户信息
   userData, _ := l.svcCtx.UserModel.FindOneByUserName(l.ctx, in.UserName)
   if userData == nil {
      return nil, errors.New("用户未注册")
   }
   //密码解密
   dePassword, dePassErr := comm.Decrypt(userData.PassWord, []byte(l.svcCtx.Config.SecretKey))
   if dePassErr != nil {
      return nil, dePassErr
   }
   if in.PassWord != dePassword {
      return nil, errors.New("密码错误")
   }
   //获取客户端IP
   ip, _ := comm.ExternalIp()
   user := model.User{
      Id:           userData.Id,
      UserIdentity: userData.UserIdentity,
      UserName:     userData.UserName,
      PassWord:     userData.PassWord,
      UserNick:     userData.UserNick,
      UserFace:     userData.UserFace,
      UserSex:      userData.UserSex,
      UserEmail:    userData.UserEmail,
      UserPhone:    userData.UserPhone,
      LoginAddress: ip.String(),
   }
   cntErr := l.svcCtx.UserModel.Update(l.ctx, &user)
   if cntErr != nil {
      return nil, cntErr
   }
   userJsonData, jsonErr := json.Marshal(userData)
   if jsonErr != nil {
      return nil, jsonErr
   }

   return &account.CommonResply{
      Code:    http.StatusOK,
      Message: "登录成功",
      Data:    string(userJsonData),
   }, nil
}
注册的代码如下:

package logic

import (
   "context"
   "errors"
   "github.com/google/uuid"
   "microshop/comm"
   "microshop/user/model"

   "microshop/user/rpc/internal/svc"
   "microshop/user/rpc/types/account"

   "github.com/zeromicro/go-zero/core/logx"
)

type RegisterLogic struct {
   ctx    context.Context
   svcCtx *svc.ServiceContext
   logx.Logger
}

func NewRegisterLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RegisterLogic {
   return &RegisterLogic{
      ctx:    ctx,
      svcCtx: svcCtx,
      Logger: logx.WithContext(ctx),
   }
}

func (l *RegisterLogic) Register(in *account.RegisterReq) (*account.CommonResply, error) {
   // todo: add your logic here and delete this line
   userData, _ := l.svcCtx.UserModel.FindOneByUserPhone(l.ctx, in.UserName)
   if userData != nil {
      return nil, errors.New("用户名已注册")
   }

   if len(in.PassWord) < 6 || len(in.PassWord) > 16 {
      return nil, errors.New("密码之能是6~16位")
   }

   if ok := comm.CheckPassword(in.PassWord); !ok {
      return nil, errors.New("密码格式错误")
   }

   if ok := comm.CheckUsername(in.UserName); !ok {
      return nil, errors.New("用户名格式错误")
   }

   enPassWord, enPassErr := comm.Encrypt(in.PassWord, []byte(l.svcCtx.Config.SecretKey))
   if enPassErr != nil {
      return nil, enPassErr
   }

   newUUID, _ := uuid.NewUUID()
   user := model.User{
      UserIdentity: newUUID.String(),
      UserName:     in.UserName,
      PassWord:     enPassWord,
      UserNick:     in.UserNick,
      UserFace:     in.UserFace,
      UserSex:      in.UserSex,
      UserEmail:    in.UserEmail,
      UserPhone:    "",
      LoginAddress: "",
   }
   cnt, cntErr := l.svcCtx.UserModel.Insert(l.ctx, &user)

   if cntErr != nil {
      return nil, cntErr
   }

   rowsAffected, rowsErr := cnt.RowsAffected()
   if rowsErr != nil {
      return nil, rowsErr
   }

   if rowsAffected == 0 {
      return nil, errors.New("注册失败")
   }

   return &account.CommonResply{
      Code:    0,
      Message: "注册成功",
   }, nil
}
查找用户信息的代码如下:

package logic

import (
   "context"
   "encoding/json"
   "microshop/comm/errorx"
   "net/http"

   "microshop/user/rpc/internal/svc"
   "microshop/user/rpc/types/account"

   "github.com/zeromicro/go-zero/core/logx"
)

type UserInfoLogic struct {
   ctx    context.Context
   svcCtx *svc.ServiceContext
   logx.Logger
}

func NewUserInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UserInfoLogic {
   return &UserInfoLogic{
      ctx:    ctx,
      svcCtx: svcCtx,
      Logger: logx.WithContext(ctx),
   }
}

func (l *UserInfoLogic) UserInfo(in *account.UserInfoReq) (*account.CommonResply, error) {
   // 获取用户信息
   userData, _ := l.svcCtx.UserModel.FindOneByUserIdentity(l.ctx, in.UserIdentity)
   if userData == nil {
      return nil, errorx.NewDefaultError("找不到该用户")
   }

   userJsonData, JsonErr := json.Marshal(userData)
   if JsonErr != nil {
      return nil, errorx.NewDefaultError("数据转换失败")
   }
   return &account.CommonResply{
      Code:    http.StatusOK,
      Message: "获取成功",
      Data:    string(userJsonData),
   }, nil
}
由于FindOneByUserIdentity不是通过命令生成的,需要自已开发,具体代码如下,位置在user_model.go文件里面

func (m *defaultUserModel) FindOneByUserIdentity(ctx context.Context, userIdentity string) (*User, error) {
   payOidKey := fmt.Sprintf("%s%v", cachePayOidPrefix, userIdentity)
   var resp User
   err := m.QueryRowCtx(ctx, &resp, payOidKey, func(ctx context.Context, conn sqlx.SqlConn, v interface{}) error {
      query := fmt.Sprintf("select %s from %s where `userIdentity` = ? limit 1", userRows, m.table)
      return conn.QueryRowCtx(ctx, v, query, userIdentity)
   })
   switch err {
   case nil:
      return &resp, nil
   case sqlc.ErrNotFound:
      return nil, ErrNotFound
   default:
      return nil, err
   }
}
 
(4)启动RPC服务 
启动之前先开启ETCD服务和redis服务,如下

进入rpc目录 运行 go run account.go,运行成功后如下

 (5)测试RPC接口

至此用户的RPC服务已经构建完成
四:用户API服务
1.编写用户API接口
在user目录下的API目录下建立api的目录下建立user.api
syntax ="v1"
info (
   title: "microShop/user.api"
   author: "jobhandsome"
   version: "1.0.0"
)
type (
   //定义注册的请求体
   RegisterReq {
      UserName  string `json:"userName"`  //用户名
      Password  string `json:"password"`  //密码
      UserNick  string `json:"userNick"`  //用户昵称
      UserFace  string `json:"userFace"`  //用户头像地址
      UserSex   int64  `json:"userSex"`   //用户性别
      UserEmail string `json:"userEmail"` //用户邮箱
   }

   //登录请求体
   LoginReq {
      UserName string `json:"userName"` //用户名
      Password string `json:"password"` //密码
   }

   UserInfoReq struct{}

   UserInfoReply {
      Code    int64         `json:"code"`
      Message string        `json:"message"`
      Data    *UserInfoItem `json:"data"`
   }
   UserInfoItem {
      UserIdentity string `json:"userIdentity"` // 用户唯一表哦是
      UserName     string `json:"userName"`     // 用户名
      UserNick     string `json:"userNick"`     // 用户昵称
      UserFace     string `json:"userFace"`     // 用户头像地址
      UserSex      int64  `json:"userSex"`      // 用户性别:0男,1女,2保密
      UserEmail    string `json:"userEmail"`    // 用户邮箱
      UserPhone    string `json:"userPhone"`    // 用户手机号
   }
   CommonResply {
      Code    int64  `json:"code"`
      Message string `json:"message"`
      Data    string `json:"data"`
   }
)
@server (
   prefix: account
)

service user-api{
   @doc(
      summary:"用户注册"
   )
   // 定义 http.HandleFunc 转换的 go 文件名称及方法,每个接口都会跟一个 handler
   @handler Register
   post /register(RegisterReq) returns (CommonResply)

   @doc(
      summary:"用户登录"
   )
   @handler Login
   post /login(LoginReq) returns(CommonResply)
}

@server(
   jwt:Auth
)

service user-api{
   @doc(
      summary:"用户信息"
   )
   @handler userInfo
   post /userinfo(UserInfoReq) returns (UserInfoReply)
}
2.生成API
进入到API目录下,运行如下命令
goctl api go -api user.api -dir . -style go-zero
运行中生成的目录如下:

 3.API配置

(1)user-api.yaml的配置
  
Name: user-api
Host: 0.0.0.0
Port: 9001
SecretKey: C8xHnG6s
# mysql配置
Mysql:
  Host: 127.0.0.1:3306
  User: root
  Pass: !!str 123456
  Data: user
  Charset: utf8mb4
# redis 配置
CacheRedis:
  - Host: 127.0.0.1:6379
    Pass:
    Type: node
BizRedis:
  Host: 127.0.0.1:6379
  Pass:
  Type: node

Auth:
  AccessSecret: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
  AccessExpire: 64800
(2)config.go的配置

 (3)服务依赖配置

 (4)API接口内部开发

登录的接口代码如下

import (
   "context"
   "encoding/json"
   "github.com/golang-jwt/jwt/v4"
   "microshop/user/model"
   "microshop/user/rpc/user"
   "time"

   "microshop/user/api/internal/svc"
   "microshop/user/api/internal/types"

   "github.com/zeromicro/go-zero/core/logx"
)

type LoginLogic struct {
   logx.Logger
   ctx    context.Context
   svcCtx *svc.ServiceContext
}

func NewLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LoginLogic {
   return &LoginLogic{
      Logger: logx.WithContext(ctx),
      ctx:    ctx,
      svcCtx: svcCtx,
   }
}

func (l *LoginLogic) Login(req *types.LoginReq) (resp *types.CommonResply, err error) {
   // todo: add your logic here and delete this line
   cnt, cntErr := l.svcCtx.Rpc.Login(l.ctx, &user.LoginReq{
      UserName: req.UserName,
      PassWord: req.Password,
   })
   if cntErr != nil {
      return nil, cntErr
   }
   var userData model.User
   userErr := json.Unmarshal([]byte(cnt.Data), &userData)
   if userErr != nil {
      return nil, userErr
   }

   //jwt
   payloads := make(map[string]any)
   payloads["userIdentity"] = userData.UserIdentity

   accessToken, tokenErr := l.GetToken(time.Now().Unix(), l.svcCtx.Config.Auth.AccessSecret, payloads, l.svcCtx.Config.Auth.AccessExpire)
   if tokenErr != nil {
      return nil, tokenErr
   }

   return &types.CommonResply{
      Code:    cnt.Code,
      Message: cnt.Message,
      Data:    accessToken,
   }, nil
}
func (l *LoginLogic) GetToken(iat int64, secretKey string, payloads map[string]any, seconds int64) (string, error) {
   claims := make(jwt.MapClaims)
   claims["expTime"] = iat + seconds
   claims["iat"] = iat
   for k, v := range payloads {
      claims[k] = v
   }
   token := jwt.New(jwt.SigningMethodHS256)
   token.Claims = claims
   return token.SignedString([]byte(secretKey))
}
注册的代码如下:

package logic

import (
   "context"
   "microshop/user/rpc/user"

   "microshop/user/api/internal/svc"
   "microshop/user/api/internal/types"

   "github.com/zeromicro/go-zero/core/logx"
)

type RegisterLogic struct {
   logx.Logger
   ctx    context.Context
   svcCtx *svc.ServiceContext
}

func NewRegisterLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RegisterLogic {
   return &RegisterLogic{
      Logger: logx.WithContext(ctx),
      ctx:    ctx,
      svcCtx: svcCtx,
   }
}

func (l *RegisterLogic) Register(req *types.RegisterReq) (resp *types.CommonResply, err error) {
   // todo: add your logic here and delete this line
   cnt, cntErr := l.svcCtx.Rpc.Register(l.ctx, &user.RegisterReq{
      UserName:  req.UserName,
      PassWord:  req.Password,
      UserNick:  req.UserNick,
      UserFace:  req.UserFace,
      UserSex:   req.UserSex,
      UserEmail: req.UserEmail,
   })
   if cntErr != nil {
      return nil, cntErr
   }

   return &types.CommonResply{
      Code:    cnt.Code,
      Message: cnt.Message,
   }, nil
}
 获取用户信息的接口如下

package logic

import (
   "context"
   "encoding/json"
   "fmt"
   "microshop/comm/errorx"
   "microshop/user/rpc/types/account"
   "net/http"

   "microshop/user/api/internal/svc"
   "microshop/user/api/internal/types"

   "github.com/zeromicro/go-zero/core/logx"
)

type UserInfoLogic struct {
   logx.Logger
   ctx    context.Context
   svcCtx *svc.ServiceContext
}

func NewUserInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UserInfoLogic {
   return &UserInfoLogic{
      Logger: logx.WithContext(ctx),
      ctx:    ctx,
      svcCtx: svcCtx,
   }
}

func (l *UserInfoLogic) UserInfo(req *types.UserInfoReq) (resp *types.UserInfoReply, err error) {
   // todo: add your logic here and delete this line
   userIdentity := fmt.Sprintf("%v", l.ctx.Value("userIdentity"))
   userData, userErr := l.svcCtx.Rpc.UserInfo(l.ctx, &account.UserInfoReq{UserIdentity: userIdentity})
   if userErr != nil {
      return nil, userErr
   }

   userInfoItem := types.UserInfoItem{}

   userJsonErr := json.Unmarshal([]byte(userData.Data), &userInfoItem)
   if userJsonErr != nil {
      return nil, errorx.NewDefaultError("数据转换失败")
   }
   return &types.UserInfoReply{
      Code:    http.StatusOK,
      Message: "获取成功",
      Data:    &userInfoItem,
   }, nil
}
 (5)启动服务
在user/api目录下运行 go run user.go,启动成功后如下图

 (6)用POSTMAN测试

 至此整个用户的微服务已经完成了。


                
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值