Go-kratos 框架微服务商城实战之用户服务(一)

                                    kratos 微服务框架商城实战之用户服务(一)

推荐看一下Kratos 官方文档[1] 更加流畅观看此文章,本机器这里已经安装好了kratos、proto、wire、make 等所需的命令工具

准备工作

初始化项目目录

进入自己电脑中存放 Go 项目的目录,

新建  kratos-shop/service 目录并进入到新建的目录中,

执行 kratos new user 命令并进入 user 目录,

执行命令 kratos proto add api/user/v1/user.proto ,这时你在 kratos-shop/service/user/api/user/v1 目录下会看到新的 user.proto 文件已经创建好了,

接下来执行 kratos proto server api/user/v1/user.proto -t internal/service 命令生成对应的 service 文件。

删除不需要的 proto 文件 rm -rf api/helloworld/

删除不需要的 service 文件 rm internal/service/greeter.go

完整的命令代码如下

mkdir  -p kratos-shop/service
cd kratos-shop/service

kratos new user
cd user

kratos proto add api/user/v1/user.proto

kratos proto server api/user/v1/user.proto -t internal/service

rm -rf api/helloworld/
rm internal/service/greeter.go

修改 user.proto 文件,内容如下:

proto 基本的语法请自行学习,目前这里只提供了一个创建用户的 rpc 接口,后续会逐步添加其他 rpc 接口

syntax = "proto3";
package user.v1;
option go_package = "user/api/user/v1;v1";

service User{
  rpc CreateUser(CreateUserInfo) returns (UserInfoResponse); // 创建用户
}

// 创建用户所需字段
message  CreateUserInfo{
  string nickName = 1;
  string password = 2;
  string mobile = 3;
}

// 返回用户信息
message UserInfoResponse{
  int64 id = 1;
  string password = 2;
  string mobile = 3;
  string nickName = 4;
  int64 birthday = 5;
  string gender = 6;
  int32 role = 7;
}

生成 user.proto 定义的接口信息

进入到 service/user 目录下,执行 make api 命令,

这时可以看到 user/api/user/v1/ 目录下多出了 proto 创建的文件

cd user

make api 

# 目录结构如下:
├── api
│   └── user
│       └── v1
│           ├── user.pb.go
│           ├── user.proto
│           └── user_grpc.pb.go

修改配置文件

修改 user/configs/config.yaml 文件,代码如下:

具体链接 mysql、redis 的参数填写自己本机的,本项目用到的是 gorm 。trace 是以后要用到的链路追踪的参数,先定义了。

server:
  http:
    addr: 0.0.0.0:8000
    timeout: 1s
  grpc:
    addr: 0.0.0.0:50051
    timeout: 1s
data:
  database:
    driver: mysql
    source: root:root@tcp(127.0.0.1:3306)/shop_user?charset=utf8mb4&parseTime=True&loc=Local
  redis:
    addr: 127.0.0.1:6379
    dial_timeout: 1s
    read_timeout: 0.2s
    write_timeout: 0.2s
trace:  endpoint: http://127.0.0.1:14268/api/traces

新建 user/configs/registry.yaml 文件,引入consul[2] 服务,代码如下:

# 这里引入了 consul 的服务注册与发现,先把配置加入进去
consul:
    address: 127.0.0.1:8500    scheme: http

修改 user/internal/conf/conf.proto 配置文件

# 文件底部新增 consul 和 trace 的配置信息
message Trace {
  string endpoint = 1;
}

message Registry {
  message Consul {
    string address = 1;
    string scheme = 2;
  }
  Consul consul = 1;}

新生成 conf.pb.go 文件,执行 make config

# `service/user` 目录下,执行命令make config

安装 consul 服务工具

# 这里使用的是 docker 工具进行创建的
docker run -d -p 8500:8500 -p 8300:8300 -p 8301:8301 -p 8302:8302 -p 8600:8600/udp consul consul agent -dev -client=0.0.0.0
# 浏览器访问 http://127.0.0.1:8500/ui/dc1/services 测试是否安装成功

修改服务代码

修改 user/internal/data/ 目录下的文件

修改 greeter.go 为 user.go 添加如下内容:

package data

import (
    "github.com/go-kratos/kratos/v2/log"
    "github.com/go-redis/redis/extra/redisotel"
    "github.com/go-redis/redis/v8"
    "github.com/google/wire"
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
    "gorm.io/gorm/logger"
    "gorm.io/gorm/schema"
    slog "log"
    "os"
    "time"
    "user/internal/conf"
)

// ProviderSet is data providers.
var ProviderSet = wire.NewSet(NewData, NewDB, NewRedis, NewUserRepo)

type Data struct {
    db  *gorm.DB
    rdb *redis.Client
}

// NewData .
func NewData(c *conf.Data, logger log.Logger, db *gorm.DB, rdb *redis.Client) (*Data, func(), error) {
    cleanup := func() {
        log.NewHelper(logger).Info("closing the data resources")
    }
    return &Data{db: db, rdb: rdb}, cleanup, nil
}

// NewDB .
func NewDB(c *conf.Data) *gorm.DB {
    // 终端打印输入 sql 执行记录
    newLogger := logger.New(
        slog.New(os.Stdout, "\r\n", slog.LstdFlags), // io writer
        logger.Config{
            SlowThreshold: time.Second, // 慢查询 SQL 阈值
            Colorful:      true,        // 禁用彩色打印
            //IgnoreRecordNotFoundError: false,
            LogLevel: logger.Info, // Log lever
        },
    )

    db, err := gorm.Open(mysql.Open(c.Database.Source), &gorm.Config{
        Logger:                                   newLogger,
        DisableForeignKeyConstraintWhenMigrating: true,
        NamingStrategy:                           schema.NamingStrategy{
            //SingularTable: true, // 表名是否加 s
        },
    })

    if err != nil {
        log.Errorf("failed opening connection to sqlite: %v", err)
        panic("failed to connect database")
    }

    return db
}

func NewRedis(c *conf.Data) *redis.Client {
    rdb := redis.NewClient(&redis.Options{
        Addr:         c.Redis.Addr,
        Password:     c.Redis.Password,
        DB:           int(c.Redis.Db),
        DialTimeout:  c.Redis.DialTimeout.AsDuration(),
        WriteTimeout: c.Redis.WriteTimeout.AsDuration(),
        ReadTimeout:  c.Redis.ReadTimeout.AsDuration(),
    })
    rdb.AddHook(redisotel.TracingHook{})
    if err := rdb.Close(); err != nil {
        log.Error(err)
    }
    return rdb}

这里的 wire 概念如果不熟悉的话,请参看Wire 依赖注入[3]

修改 user/internal/service/ 目录下的文件

修改或者删除 greeter.go 为 user.go, 添加代码如下:

package service

import (
    "context"
    "github.com/go-kratos/kratos/v2/log"
    v1 "user/api/user/v1"
    "user/internal/biz"
)

type UserService struct {
    v1.UnimplementedUserServer

    uc  *biz.UserUsecase
    log *log.Helper
}

// NewUserService new a greeter service.
func NewUserService(uc *biz.UserUsecase, logger log.Logger) *UserService {
    return &UserService{uc: uc, log: log.NewHelper(logger)}
}

// CreateUser create a user
func (u *UserService) CreateUser(ctx context.Context, req *v1.CreateUserInfo) (*v1.UserInfoResponse, error) {
    user, err := u.uc.Create(ctx, &biz.User{
        Mobile:   req.Mobile,
        Password: req.Password,
        NickName: req.NickName,
    })
    if err != nil {
        return nil, err
    }

    userInfoRsp := v1.UserInfoResponse{
        Id:       user.ID,
        Mobile:   user.Mobile,
        Password: user.Password,
        NickName: user.NickName,
        Gender:   user.Gender,
        Role:     int32(user.Role),
        Birthday: user.Birthday,
    }

    return &userInfoRsp, nil
}

修改 service.go 文件, 代码如下:

package service

import "github.com/google/wire"

// ProviderSet is service providers.
var ProviderSet = wire.NewSet(NewUserService)

修改或删除 user/internal/biz/greeter.go 为 user.go 添加如下内容:

package biz

import (
    "context"
    "github.com/go-kratos/kratos/v2/log"
)

// 定义返回数据结构体
type User struct {
    ID       int64
    Mobile   string
    Password string
    NickName string
    Birthday int64
    Gender   string
    Role     int
}

type UserRepo interface {
    CreateUser(context.Context, *User) (*User, error)
}

type UserUsecase struct {
    repo UserRepo
    log  *log.Helper
}

func NewUserUsecase(repo UserRepo, logger log.Logger) *UserUsecase {
    return &UserUsecase{repo: repo, log: log.NewHelper(logger)}
}

func (uc *UserUsecase) Create(ctx context.Context, u *User) (*User, error) {
    return uc.repo.CreateUser(ctx, u)
}

修改 user/internal/biz/biz.go 文件,内容如下:

package biz

import "github.com/google/wire"

// ProviderSet is biz providers.
var ProviderSet = wire.NewSet(NewUserUsecase)

修改或删除 user/internal/data/greeter.go 为 user.go 添加如下内容:

package data

import (
    "context"
    "crypto/sha512"
    "fmt"
    "github.com/anaskhan96/go-password-encoder"
    "github.com/go-kratos/kratos/v2/log"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
    "gorm.io/gorm"
    "time"
    "user/internal/biz"
)
// 定义数据表结构体
type User struct {
    ID          int64      `gorm:"primarykey"`
    Mobile      string     `gorm:"index:idx_mobile;unique;type:varchar(11) comment '手机号码,用户唯一标识';not null"`
    Password    string     `gorm:"type:varchar(100);not null "` // 用户密码的保存需要注意是否加密
    NickName    string     `gorm:"type:varchar(25) comment '用户昵称'"`
    Birthday    *time.Time `gorm:"type:datetime comment '出生日日期'"`
    Gender      string     `gorm:"column:gender;default:male;type:varchar(16) comment 'female:女,male:男'"`
    Role        int        `gorm:"column:role;default:1;type:int comment '1:普通用户,2:管理员'"`
    CreatedAt   time.Time  `gorm:"column:add_time"`
    UpdatedAt   time.Time  `gorm:"column:update_time"`
    DeletedAt   gorm.DeletedAt
    IsDeletedAt bool
}
type userRepo struct {
    data *Data
    log  *log.Helper
}

// NewUserRepo . 这里需要注意,上面 data 文件 wire 注入的是此方法,方法名不要写错了
func NewUserRepo(data *Data, logger log.Logger) biz.UserRepo {
    return &userRepo{
        data: data,
        log:  log.NewHelper(logger),
    }
}

// CreateUser .
func (r *userRepo) CreateUser(ctx context.Context, u *biz.User) (*biz.User, error) {
    var user User
    // 验证是否已经创建
    result := r.data.db.Where(&biz.User{Mobile: u.Mobile}).First(&user)
    if result.RowsAffected == 1 {
        return nil, status.Errorf(codes.AlreadyExists, "用户已存在")
    }

    user.Mobile = u.Mobile
    user.NickName = u.NickName
    user.Password = encrypt(u.Password) // 密码加密
    res := r.data.db.Create(&user)
    if res.Error != nil {
        return nil, status.Errorf(codes.Internal, res.Error.Error())
    }
    return &biz.User{
        ID:       user.ID,
        Mobile:   user.Mobile,
        Password: user.Password,
        NickName: user.NickName,
        Gender:   user.Gender,
        Role:     user.Role,
    }, nil
}

// Password encryption
func encrypt(psd string) string {
    options := &password.Options{SaltLen: 16, Iterations: 10000, KeyLen: 32, HashFunction: sha512.New}
    salt, encodedPwd := password.Encode(psd, options)
    return fmt.Sprintf("$pbkdf2-sha512$%s$%s", salt, encodedPwd)
}

修改 user/internal/server/ 目录下的文件

这里用不到 http 服务删除 http.go 文件,修改 grpc.go 文件内容如下:

 package server

import (
    "github.com/go-kratos/kratos/v2/log"
    "github.com/go-kratos/kratos/v2/middleware/logging"
    "github.com/go-kratos/kratos/v2/middleware/recovery"
    "github.com/go-kratos/kratos/v2/transport/grpc"
    v1 "user/api/user/v1"
    "user/internal/conf"
    "user/internal/service"
)

// NewGRPCServer new a gRPC server.
func NewGRPCServer(c *conf.Server, greeter *service.UserService, logger log.Logger) *grpc.Server {
    var opts = []grpc.ServerOption{
        grpc.Middleware(
            recovery.Recovery(),
            logging.Server(logger),
        ),
    }
    if c.Grpc.Network != "" {
        opts = append(opts, grpc.Network(c.Grpc.Network))
    }
    if c.Grpc.Addr != "" {
        opts = append(opts, grpc.Address(c.Grpc.Addr))
    }
    if c.Grpc.Timeout != nil {
        opts = append(opts, grpc.Timeout(c.Grpc.Timeout.AsDuration()))
    }
    srv := grpc.NewServer(opts...)
    v1.RegisterUserServer(srv, greeter)
    return srv
}

修改 server.go 文件,这里加入了 consul 的服务,内容如下:

package server

import (
    "github.com/go-kratos/kratos/v2/registry"
    "github.com/google/wire"
    "user/internal/conf"

    consul "github.com/go-kratos/kratos/contrib/registry/consul/v2"
    consulAPI "github.com/hashicorp/consul/api"
)

// ProviderSet is server providers.
var ProviderSet = wire.NewSet(NewGRPCServer, NewRegistrar)

// NewRegistrar 引入 consul
func NewRegistrar(conf *conf.Registry) registry.Registrar {
    c := consulAPI.DefaultConfig()
    c.Address = conf.Consul.Address
    c.Scheme = conf.Consul.Scheme

    cli, err := consulAPI.NewClient(c)
    if err != nil {
        panic(err)
    }
    r := consul.New(cli, consul.WithHealthCheck(false))
    return r
}

修改启动程序

修改 user/cmd/wire.go文件

这里注入了consul需要的配置,需要添加进来

func initApp(*conf.Server, *conf.Data, *conf.Registry, log.Logger) (*kratos.App, func(), error) {
    panic(wire.Build(server.ProviderSet, data.ProviderSet, biz.ProviderSet, service.ProviderSet, newApp))}

修改 user/cmd/user/main.go 文件

package main

import (
    "flag"
    "os"

    "github.com/go-kratos/kratos/v2"
    "github.com/go-kratos/kratos/v2/config"
    "github.com/go-kratos/kratos/v2/config/file"
    "github.com/go-kratos/kratos/v2/log"
    "github.com/go-kratos/kratos/v2/middleware/tracing"
    "github.com/go-kratos/kratos/v2/registry"
    "github.com/go-kratos/kratos/v2/transport/grpc"
    "user/internal/conf"
)

// go build -ldflags "-X main.Version=x.y.z"
var (
    // Name is the name of the compiled software.
    Name = "shop.users.service"
    // Version is the version of the compiled software.
    Version = "v1"
    // flagconf is the config flag.
    flagconf string

    id, _ = os.Hostname()
)

func init() {
    flag.StringVar(&flagconf, "conf", "../../configs", "config path, eg: -conf config.yaml")
}

func newApp(logger log.Logger, gs *grpc.Server, rr registry.Registrar) *kratos.App {
    return kratos.New(
        kratos.ID(id+"shop.user.service"),
        kratos.Name(Name),
        kratos.Version(Version),
        kratos.Metadata(map[string]string{}),
        kratos.Logger(logger),
        kratos.Server(
            gs,
        ),
        kratos.Registrar(rr), // consul 的引入
    )
}

func main() {
    flag.Parse()
    logger := log.With(log.NewStdLogger(os.Stdout),
        "ts", log.DefaultTimestamp,
        "caller", log.DefaultCaller,
        "service.id", id,
        "service.name", Name,
        "service.version", Version,
        "trace_id", tracing.TraceID(),
        "span_id", tracing.SpanID(),
    )
    c := config.New(
        config.WithSource(
            file.NewSource(flagconf),
        ),
    )
    defer c.Close()

    if err := c.Load(); err != nil {
        panic(err)
    }

    var bc conf.Bootstrap
    if err := c.Scan(&bc); err != nil {
        panic(err)
    }
    // consul 的引入
    var rc conf.Registry 
    if err := c.Scan(&rc); err != nil {
        panic(err)
    }
    app, cleanup, err := initApp(bc.Server, bc.Data, &rc, logger)
    if err != nil {
        panic(err)
    }
    defer cleanup()

    // start and wait for stop signal
    if err := app.Run(); err != nil {
        panic(err)
    }
}

修改根目录 user/makefile 文件

    在 go generate ./... 下面添加代码

    wire:        cd cmd/user/ && wire

根目录执行 make wire 命令

# service/usermake wire

启动程序

别忘记根据 data 里面的 user struct 创建对应的数据库表,这里也可以写一个 gorm 创建表的文件进行创建。

启动程序 kratos run

根目录 service/user 执行命令    kratos run

简单测试

由于没写对外访问的 http 服务,这里还没有加入单元测试,所以先创建个文件链接启动过的 grpc 服务简单测试一下。

根目录新建 user/test/user.go 文件,添加如下内容:

package main

import (
    "context"
    "fmt"
    "google.golang.org/grpc"
    v1 "user/api/user/v1"
)

var userClient v1.UserClient
var conn *grpc.ClientConn

func main() {
    Init()

    TestCreateUser() // 创建用户

    conn.Close()
}

// Init 初始化 grpc 链接 注意这里链接的 端口
func Init() {
    var err error
    conn, err = grpc.Dial("127.0.0.1:50051", grpc.WithInsecure())
    if err != nil {
        panic("grpc link err" + err.Error())
    }
    userClient = v1.NewUserClient(conn)
}

func TestCreateUser() {

    rsp, err := userClient.CreateUser(context.Background(), &v1.CreateUserInfo{
        Mobile:   fmt.Sprintf("1388888888%d", 1),
        Password: "admin123",
        NickName: fmt.Sprintf("YWWW%d", 1),
    })
    if err != nil {
        panic("grpc 创建用户失败" + err.Error())
    }
    fmt.Println(rsp.Id)}

这里别忘记启动 kratos user 服务之后,再执行 test/user.go 文件,查询执行结果,是否有个ID输出 查询自己的数据库,看看是否有插入的数据了。

源码已经上传到 GitHub[4] 上了,下一篇开始逐步完善用户服务的接口。

上帝喜欢笨人
我的想法很简单,既然自己能力有限,就按照有限的能力尽力而为就好,不要以为有了什么高科技的时间管理工具就能做更多事情。人有多大本事,做多大事情。
36篇原创内容
公众号

Reference

Go工程化-依赖注入 https://go-kratos.dev/blog/go-project-wire

Project Layout 最佳实践 https://go-kratos.dev/blog/go-layout-operation-process

引用链接

[1] Kratos 官方文档: https://go-kratos.dev/docs/getting-started/start
[2] consul: https://www.consul.io/
[3] Wire 依赖注入: https://go-kratos.dev/docs/guide/wire

[4] GitHub: https://github.com/aliliin/kratos-shop

本文使用 文章同步助手 同步

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容