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.go和wire.go将 conf.Auth和conf.Registry引入,和之前操作类似,这里不再详述。
依赖注入
go generate ./...