Go-kratos 框架商城微服务实战七

288 阅读7分钟

Go-kratos 框架商城微服务实战七

新增shop

kratos new app/shop --nomod

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

修改proto文件

syntax = "proto3";

package api.shop.v1;
// 这里可以把 proto 文件下载下来,放到项目的 third_party 目录下
import "google/api/annotations.proto";
import "google/protobuf/empty.proto";
import "validate/validate.proto";

option go_package = "shop/api/shop/v1;v1";
// The Shop service definition.
service Shop {
  rpc Register (RegisterReq) returns (RegisterReply) {
    option (google.api.http) = {
      post: "/api/users/register",
      body: "*",
    };
  }
  rpc Login (LoginReq) returns (RegisterReply) {
    option (google.api.http) = {
      post: "/api/users/login",
      body: "*",
    };
  }
  rpc Captcha (google.protobuf.Empty) returns (CaptchaReply) {
    option (google.api.http) = {
      get: "/api/users/captcha",
    };
  }
  rpc Detail (google.protobuf.Empty) returns (UserDetailResponse) {
    option (google.api.http) = {
      get: "/api/users/detail",
    };
  }
  rpc CreateAddress (CreateAddressReq) returns (AddressInfo) {
    option (google.api.http) = {
      post: "/api/address/create",
      body: "*",
    };
  }
  rpc AddressListByUid (google.protobuf.Empty) returns (ListAddressReply) {
    option (google.api.http) = {
      get: "/api/address/list/uid",
    };
  }
  rpc UpdateAddress (UpdateAddressReq) returns (CheckResponse) {
    option (google.api.http) = {
      put: "/api/address/update",
      body: "*",
    };
  }
  rpc DefaultAddress (AddressReq) returns (CheckResponse) {
    option (google.api.http) = {
      put: "/api/address/default",
      body: "*",
    };
  }
  rpc DeleteAddress (AddressReq) returns (CheckResponse) {
    option (google.api.http) = {
      delete: "/api/address/delete",
    };
  }
}

message CreateAddressReq {
  int64 uid = 1;
  string name = 2 [(validate.rules).string ={min_len: 1}];
  string mobile = 3 [(validate.rules).string.len = 11];
  string Province = 4 [(validate.rules).string ={min_len: 1}];
  string City = 5 [(validate.rules).string ={min_len: 1}];
  string Districts = 6 [(validate.rules).string ={min_len: 1}];
  string address = 7 [(validate.rules).string ={min_len: 1}];
  string post_code = 8;
  int32 is_default = 9;
}

message UpdateAddressReq {
  int64 uid = 1;
  string name = 2;
  string mobile = 3 [(validate.rules).string.len = 11];
  string Province = 4 [(validate.rules).string ={min_len: 1}];
  string City = 5 [(validate.rules).string ={min_len: 1}];
  string Districts = 6 [(validate.rules).string ={min_len: 1}];
  string address = 7 [(validate.rules).string ={min_len: 1}];
  string post_code = 8;
  int32 is_default = 9;
  int64 id = 10 [(validate.rules).int64.gte = 1];
}

message AddressInfo {
  int64 id = 1;
  string name = 2 [(validate.rules).string ={min_len: 1}];
  string mobile = 3 [(validate.rules).string.len = 11];
  string Province = 4;
  string City = 5;
  string Districts = 6;
  string address = 7;
  string post_code = 8;
  int32 is_default = 9;
}

message ListAddressReq {
  int64 uid = 1;
}

message ListAddressReply {
  repeated AddressInfo results = 1;
}

message AddressReq {
  int64 id = 1 [(validate.rules).int64.gte = 1];
  int64 uid = 2;
}

message CheckResponse{
  bool success = 1;
}

// Data returned by registration and login
message RegisterReply {
  int64 id = 1;
  string mobile = 3;
  string username = 4;
  string token = 5;
  int64 expiredAt = 6;
}

message RegisterReq {
  string mobile = 1 [(validate.rules).string.len = 11];
  string username = 2 [(validate.rules).string = {min_len: 3, max_len: 15}];
  string password = 3 [(validate.rules).string = {min_len: 8}];
}

message LoginReq {
  string mobile = 1 [(validate.rules).string.len = 11];
  string password = 2 [(validate.rules).string = {min_len: 8}];
  string captcha = 3 [(validate.rules).string = {min_len: 5,max_len:5}];
  string captchaId = 4  [(validate.rules).string ={min_len: 1}];
}

// user Detail returned
message UserDetailResponse{
  int64 id = 1;
  string mobile = 2;
  string nickName = 3;
  int64 birthday = 4;
  string gender = 5;
  int32 role = 6;
}

message CaptchaReply{
  string captchaId = 1;
  string picPath = 2;
}
  • 生成go文件
make api
  • 生成对应service层代码
kratos proto server api/shop/v1/user.proto -t app/shop/internal/service

第三方功能增加

  • 新建 /app/shop/pkg/captcha/captcha.go
package captcha

import (
	"context"

	"github.com/mojocn/base64Captcha"
)

var Store = base64Captcha.DefaultMemStore

type CaptchaInfo struct {
	CaptchaId string
	PicPath   string
}

// GetCaptcha 生成验证码
func GetCaptcha(ctx context.Context) (*CaptchaInfo, error) {
	driver := base64Captcha.NewDriverDigit(80, 250, 5, 0.7, 80)
	cp := base64Captcha.NewCaptcha(driver, Store)
	id, b64s, _, err := cp.Generate()
	if err != nil {
		return nil, err
	}

	return &CaptchaInfo{
		CaptchaId: id,
		PicPath:   b64s,
	}, nil
}
  • 新建 /app/shop/pkg/middleware/auth/auth.go
package auth

import (
	"errors"

	"github.com/golang-jwt/jwt/v4"
)

type CustomClaims struct {
	ID          int64
	NickName    string
	AuthorityId int
	jwt.StandardClaims
}

// CreateToken generate token
func CreateToken(c CustomClaims, key string) (string, error) {
	claims := jwt.NewWithClaims(jwt.SigningMethodHS256, c)
	signedString, err := claims.SignedString([]byte(key))
	if err != nil {
		return "", errors.New("generate token failed" + err.Error())
	}
	return signedString, nil
}

修改 /app/shop/internal/biz/shop.go

package biz

import (
	"context"
	"errors"
	v1 "kratos-shop/api/shop/v1"
	"kratos-shop/app/shop/internal/conf"
	"kratos-shop/app/shop/pkg/captcha"
	"kratos-shop/app/shop/pkg/middleware/auth"

	"github.com/go-kratos/kratos/v2/log"
	"github.com/go-kratos/kratos/v2/middleware/auth/jwt"
	jwt4 "github.com/golang-jwt/jwt/v4"
	jwt5 "github.com/golang-jwt/jwt/v5"

	"time"
)

// 定义错误信息
var (
	ErrPasswordInvalid     = errors.New("password invalid")
	ErrUsernameInvalid     = errors.New("username invalid")
	ErrCaptchaInvalid      = errors.New("verification code error")
	ErrMobileInvalid       = errors.New("mobile invalid")
	ErrLoginFailed         = errors.New("login failed")
	ErrGenerateTokenFailed = errors.New("generate token failed")
	ErrAuthFailed          = errors.New("authentication failed")
)

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

type UserRepo interface {
	CreateUser(c context.Context, u *User) (*User, error)
	UserByMobile(ctx context.Context, mobile string) (*User, error)
	UserById(ctx context.Context, Id int64) (*User, error)
	CheckPassword(ctx context.Context, password, encryptedPassword string) (bool, error)
}

type UserUsecase struct {
	uRepo      UserRepo
	log        *log.Helper
	signingKey string // 这里是为了生存 token 的时候可以直接取配置文件里面的配置
}

func NewUserUsecase(repo UserRepo, logger log.Logger, conf *conf.Auth) *UserUsecase {
	helper := log.NewHelper(log.With(logger, "module", "usecase/shop"))
	return &UserUsecase{uRepo: repo, log: helper, signingKey: conf.JwtKey}
}

// GetCaptcha 验证码
func (uc *UserUsecase) GetCaptcha(ctx context.Context) (*v1.CaptchaReply, error) {
	captchaInfo, err := captcha.GetCaptcha(ctx)
	if err != nil {
		return nil, err
	}

	return &v1.CaptchaReply{
		CaptchaId: captchaInfo.CaptchaId,
		PicPath:   captchaInfo.PicPath,
	}, nil
}

func (uc *UserUsecase) UserDetailByID(ctx context.Context) (*v1.UserDetailResponse, error) {
	// 在上下文 context 中取出 claims 对象
	var uId int64
	if claims, ok := jwt.FromContext(ctx); ok {
		c := claims.(jwt5.MapClaims)
		if c["ID"] == nil {
			return nil, ErrAuthFailed
		}
		uId = int64(c["ID"].(float64))
	}

	user, err := uc.uRepo.UserById(ctx, uId)
	if err != nil {
		return nil, err
	}
	return &v1.UserDetailResponse{
		Id:       user.ID,
		NickName: user.NickName,
		Mobile:   user.Mobile,
	}, nil
}

func (uc *UserUsecase) PassWordLogin(ctx context.Context, req *v1.LoginReq) (*v1.RegisterReply, error) {
	// 表单验证
	if len(req.Mobile) <= 0 {
		return nil, ErrMobileInvalid
	}
	if len(req.Password) <= 0 {
		return nil, ErrUsernameInvalid
	}
	// 验证验证码是否正确
	if !captcha.Store.Verify(req.CaptchaId, req.Captcha, true) {
		return nil, ErrCaptchaInvalid
	}

	if user, err := uc.uRepo.UserByMobile(ctx, req.Mobile); err != nil {
		return nil, ErrUserNotFound
	} else {
		// 用户存在检查密码
		if passRsp, pasErr := uc.uRepo.CheckPassword(ctx, req.Password, user.Password); pasErr != nil {
			return nil, ErrPasswordInvalid
		} else {
			if passRsp {
				claims := auth.CustomClaims{
					ID:          user.ID,
					NickName:    user.NickName,
					AuthorityId: user.Role,
					StandardClaims: jwt4.StandardClaims{
						NotBefore: time.Now().Unix(),               // 签名的生效时间
						ExpiresAt: time.Now().Unix() + 60*60*24*30, // 30天过期
						Issuer:    "Gyl",
					},
				}

				token, err := auth.CreateToken(claims, uc.signingKey)
				if err != nil {
					return nil, ErrGenerateTokenFailed
				}
				return &v1.RegisterReply{
					Id:        user.ID,
					Mobile:    user.Mobile,
					Username:  user.NickName,
					Token:     token,
					ExpiredAt: time.Now().Unix() + 60*60*24*30,
				}, nil
			} else {
				return nil, ErrLoginFailed
			}
		}
	}
}

func (uc *UserUsecase) CreateUser(ctx context.Context, req *v1.RegisterReq) (*v1.RegisterReply, error) {
	newUser, err := NewUser(req.Mobile, req.Username, req.Password)
	if err != nil {
		return nil, err
	}
	createUser, err := uc.uRepo.CreateUser(ctx, &newUser)
	if err != nil {
		return nil, err
	}
	claims := auth.CustomClaims{
		ID:          createUser.ID,
		NickName:    createUser.NickName,
		AuthorityId: createUser.Role,
		StandardClaims: jwt4.StandardClaims{
			NotBefore: time.Now().Unix(),               // 签名的生效时间
			ExpiresAt: time.Now().Unix() + 60*60*24*30, // 30天过期
			Issuer:    "Gyl",
		},
	}
	token, err := auth.CreateToken(claims, uc.signingKey)
	if err != nil {
		return nil, err
	}

	return &v1.RegisterReply{
		Id:        createUser.ID,
		Mobile:    createUser.Mobile,
		Username:  createUser.NickName,
		Token:     token,
		ExpiredAt: time.Now().Unix() + 60*60*24*30,
	}, nil
}

func NewUser(mobile, username, password string) (User, error) {
	// check mobile
	if len(mobile) <= 0 {
		return User{}, ErrMobileInvalid
	}
	// check username
	if len(username) <= 0 {
		return User{}, ErrUsernameInvalid
	}
	// check password
	if len(password) <= 0 {
		return User{}, ErrPasswordInvalid
	}
	return User{
		Mobile:   mobile,
		NickName: username,
		Password: password,
	}, nil
}

记得把NewUserUsecase注册到服务

data 层修改

  • 修改 /app/shop/internal/data/data.go
package data

import (
	"context"
	userV1 "kratos-shop/api/user/v1"
	"kratos-shop/app/shop/internal/conf"
	"time"

	consul "github.com/go-kratos/kratos/contrib/registry/consul/v2"
	"github.com/go-kratos/kratos/v2/log"
	"github.com/go-kratos/kratos/v2/middleware/recovery"
	"github.com/go-kratos/kratos/v2/middleware/tracing"
	"github.com/go-kratos/kratos/v2/registry"
	"github.com/go-kratos/kratos/v2/transport/grpc"
	"github.com/google/wire"
	consulAPI "github.com/hashicorp/consul/api"
)

// ProviderSet is data providers.
var ProviderSet = wire.NewSet(NewData, NewUserRepo, NewUserServiceClient, NewRegistrar, NewDiscovery)

// Data .
type Data struct {
	log *log.Helper
	uc  userV1.UserClient // 用户服务的客户端
}

// NewData .
func NewData(c *conf.Data, uc userV1.UserClient, logger log.Logger) (*Data, error) {
	l := log.NewHelper(log.With(logger, "module", "data"))
	return &Data{log: l, uc: uc}, nil
}

// NewUserServiceClient 链接用户服务
func NewUserServiceClient(ac *conf.Auth, sr *conf.Service, rr registry.Discovery) userV1.UserClient {
	conn, err := grpc.DialInsecure(
		context.Background(),
		grpc.WithEndpoint(sr.User.Endpoint), // consul
		grpc.WithDiscovery(rr),              // consul
		grpc.WithMiddleware(
			recovery.Recovery(),
			tracing.Client(),
		),
		grpc.WithTimeout(2*time.Second),
	)
	if err != nil {
		panic(err)
	}
	c := userV1.NewUserClient(conn)
	return c
}

// NewRegistrar add 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
}

func NewDiscovery(conf *conf.Registry) registry.Discovery {
	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
}
  • 新增 /app/shop/internal/data/shop.go
package data

import (
	"context"
	userService "kratos-shop/api/user/v1"
	"kratos-shop/app/shop/internal/biz"

	"github.com/go-kratos/kratos/v2/log"
)

type userRepo struct {
	data *Data
	log  *log.Helper
}

// NewUserRepo .
func NewUserRepo(data *Data, logger log.Logger) biz.UserRepo {
	return &userRepo{
		data: data,
		log:  log.NewHelper(log.With(logger, "module", "repo/user")),
	}
}

func (u *userRepo) CreateUser(c context.Context, user *biz.User) (*biz.User, error) {
	createUser, err := u.data.uc.CreateUser(c, &userService.CreateUserRequest{
		NickName: user.NickName,
		Password: user.Password,
		Mobile:   user.Mobile,
	})
	if err != nil {
		return nil, err
	}
	return &biz.User{
		ID:       createUser.Id,
		Mobile:   createUser.Mobile,
		NickName: createUser.NickName,
	}, nil
}

func (u *userRepo) UserByMobile(c context.Context, mobile string) (*biz.User, error) {
	byMobile, err := u.data.uc.GetUser(c, &userService.GetUserRequest{Id: 0})
	if err != nil {
		return nil, err
	}
	return &biz.User{
		Mobile:   byMobile.Mobile,
		ID:       byMobile.Id,
		NickName: byMobile.NickName,
	}, nil
}

func (u *userRepo) CheckPassword(c context.Context, password, encryptedPassword string) (bool, error) {
	if byMobile, err := u.data.uc.CheckPassword(c, &userService.PasswordCheckInfo{Password: password, EncryptedPassword: encryptedPassword}); err != nil {
		return false, err
	} else {
		return byMobile.Success, nil
	}
}

func (u *userRepo) UserById(c context.Context, id int64) (*biz.User, error) {
	user, err := u.data.uc.GetUser(c, &userService.GetUserRequest{Id: id})
	if err != nil {
		return nil, err
	}
	return &biz.User{
		ID:       user.Id,
		Mobile:   user.Mobile,
		NickName: user.NickName,
		Gender:   user.Gender,
		Role:     int(user.Role),
	}, nil
}

server层修改

  • http 服务修改
package server

import (
	"context"
	v2 "kratos-shop/api/shop/v1"
	"kratos-shop/app/shop/internal/conf"
	"kratos-shop/app/shop/internal/service"

	"github.com/go-kratos/kratos/v2/log"
	"github.com/go-kratos/kratos/v2/middleware/auth/jwt"
	"github.com/go-kratos/kratos/v2/middleware/logging"
	"github.com/go-kratos/kratos/v2/middleware/metadata"
	"github.com/go-kratos/kratos/v2/middleware/recovery"
	"github.com/go-kratos/kratos/v2/middleware/selector"
	"github.com/go-kratos/kratos/v2/middleware/tracing"
	"github.com/go-kratos/kratos/v2/middleware/validate"
	"github.com/go-kratos/kratos/v2/transport/http"
	jwtv5 "github.com/golang-jwt/jwt/v5"
	"github.com/gorilla/handlers"
)

// NewHTTPServer new an HTTP server.
func NewHTTPServer(c *conf.Server, ac *conf.Auth, s *service.ShopService, logger log.Logger) *http.Server {
	var opts = []http.ServerOption{
		http.Middleware(
			recovery.Recovery(),
			validate.Validator(),
			tracing.Server(),
			selector.Server(
				jwt.Server(func(token *jwtv5.Token) (interface{}, error) {
					return []byte(ac.JwtKey), nil
				}),
			).Match(NewWhiteListMatcher()).Build(), // 设置白名单
			metadata.Server(),
			logging.Server(logger),
		),
		http.Filter(handlers.CORS( // 浏览器跨域
			handlers.AllowedHeaders([]string{"X-Requested-With", "Content-Type", "Authorization"}),
			handlers.AllowedMethods([]string{"GET", "POST", "PUT", "HEAD", "OPTIONS"}),
			handlers.AllowedOrigins([]string{"*"}),
		)),
	}
	if c.Http.Network != "" {
		opts = append(opts, http.Network(c.Http.Network))
	}
	if c.Http.Addr != "" {
		opts = append(opts, http.Address(c.Http.Addr))
	}
	if c.Http.Timeout != nil {
		opts = append(opts, http.Timeout(c.Http.Timeout.AsDuration()))
	}
	srv := http.NewServer(opts...)
	v2.RegisterShopHTTPServer(srv, s)
	return srv
}

// NewWhiteListMatcher 设置白名单,不需要 token 验证的接口
func NewWhiteListMatcher() selector.MatchFunc {
	whiteList := make(map[string]struct{})
	whiteList["/api.shop.v1.Shop/Captcha"] = struct{}{}
	whiteList["/api.shop.v1.Shop/Login"] = struct{}{}
	whiteList["/api.shop.v1.Shop/Register"] = struct{}{}
	return func(ctx context.Context, operation string) bool {
		if _, ok := whiteList[operation]; ok {
			return false
		}
		return true
	}
}

config修改

syntax = "proto3";
package shop.api;

option go_package = "shop/internal/conf;conf";

import "google/protobuf/duration.proto";

message Bootstrap {
  Server server = 1;
  Data data = 2;
  Trace trace = 3; // 链路追踪
  Auth auth = 4; // 认证鉴权
  Service service = 5; // 服务注册与发现
}

message Server {
  message HTTP {
    string network = 1;
    string addr = 2;
    google.protobuf.Duration timeout = 3;
  }
  message GRPC {
    string network = 1;
    string addr = 2;
    google.protobuf.Duration timeout = 3;
  }
  HTTP http = 1;
  GRPC grpc = 2;
}

message Data {
  message Database {
    string driver = 1;
    string source = 2;
  }
  message Redis {
    string network = 1;
    string addr = 2;
    google.protobuf.Duration read_timeout = 3;
    google.protobuf.Duration write_timeout = 4;
  }
  Database database = 1;
  Redis redis = 2;
}

message Service {
  message User { // 用户服务
    string endpoint = 1;
  }
  message Goods { // 商品服务
    string endpoint = 1;
  }
  User user = 1;
  Goods goods = 2;
}

message Trace {
  string endpoint = 1;
}

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

message Auth {
  string jwt_key = 1;
}
  • 修改对应的config.yaml
name: shop.api
server:
  http:
    addr: 0.0.0.0:8098
    timeout: 1s
  grpc:
    addr: 0.0.0.0:9005
    timeout: 1s
data:
  database:
    driver: mysql
    source: root:root@tcp(127.0.0.1:3306)/test
  redis:
    addr: 127.0.0.1:6379
    read_timeout: 0.2s
    write_timeout: 0.2s
trace:
  endpoint: http://127.0.0.1:14268/api/traces
auth:
  jwt_key: hqFr%3ddt32DGlSTOI5cO6@TH#fFwYnP$S
service:
  user:
    endpoint: discovery:///shop.user.service
  goods:
    endpoint: discovery:///shop.goods.service

注意这里的endpoint: discovery:///shop.user.service 需要对应 /app/user/cmd/main.go 中的 Name

  • 修改对应的registry.yaml
consul:
  address: 127.0.0.1:8500
  scheme: http

service层代码修改

  • shop.go
package service

import (
	"context"

	pb "kratos-shop/api/shop/v1"
	v1 "kratos-shop/api/shop/v1"
	"kratos-shop/app/shop/internal/biz"

	"github.com/go-kratos/kratos/v2/log"
	"google.golang.org/protobuf/types/known/emptypb"
)

type ShopService struct {
	v1.UnimplementedShopServer

	uc  *biz.UserUsecase
	log *log.Helper
}

// NewShopService new a shop service.
func NewShopService(uc *biz.UserUsecase, logger log.Logger) *ShopService {
	return &ShopService{
		uc:  uc,
		log: log.NewHelper(log.With(logger, "module", "service/shop")),
	}
}

func (s *ShopService) Register(ctx context.Context, req *pb.RegisterReq) (*pb.RegisterReply, error) {
	return s.uc.CreateUser(ctx, req)
}
func (s *ShopService) Login(ctx context.Context, req *pb.LoginReq) (*pb.RegisterReply, error) {
	return s.uc.PassWordLogin(ctx, req)
}
func (s *ShopService) Captcha(ctx context.Context, req *emptypb.Empty) (*pb.CaptchaReply, error) {
	return &pb.CaptchaReply{}, nil
}
func (s *ShopService) Detail(ctx context.Context, req *emptypb.Empty) (*pb.UserDetailResponse, error) {
	return &pb.UserDetailResponse{}, nil
}
func (s *ShopService) CreateAddress(ctx context.Context, req *pb.CreateAddressReq) (*pb.AddressInfo, error) {
	return &pb.AddressInfo{}, nil
}
func (s *ShopService) AddressListByUid(ctx context.Context, req *emptypb.Empty) (*pb.ListAddressReply, error) {
	return &pb.ListAddressReply{}, nil
}
func (s *ShopService) UpdateAddress(ctx context.Context, req *pb.UpdateAddressReq) (*pb.CheckResponse, error) {
	return &pb.CheckResponse{}, nil
}
func (s *ShopService) DefaultAddress(ctx context.Context, req *pb.AddressReq) (*pb.CheckResponse, error) {
	return &pb.CheckResponse{}, nil
}
func (s *ShopService) DeleteAddress(ctx context.Context, req *pb.AddressReq) (*pb.CheckResponse, error) {
	return &pb.CheckResponse{}, nil
}

记得把NewShopService注册到服务


需要修改main.gowire.goconf.Authconf.Registry引入,和之前操作类似,这里不再详述。

依赖注入

go generate ./...