03-阶段三:从0到1实现完整的微服务框架
第08周 用户服务的grpc服务
第1章用户服务-service开发
1-1定义用户表结构
// user_srv/model/user.go
package model
import (
"gorm.io/gorm"
"time"
)
type BaseModel struct {
// 用自己的model(而不是gorm的)方便扩展
ID int32 `gorm:"primarykey"`
CreatedAt time.Time `gorm:"column:add_time"`
UpdatedAt time.Time `gorm:"column:update_time"`
DeletedAt gorm.DeletedAt
//IsDeleted bool `gorm:"column:is_deleted"`
IsDeleted bool
}
type User struct {
// 继承基本模型
BaseModel
// 自定义索引: idx_mobile, 查询比较快
// Mobile 用户手机号
Mobile string `gorm:"index:idx_mobile;unique;type:varchar(11);not null"`
// Password 密码,存入数据库会加盐
Password string `gorm:"type:varchar(100);not null"`
// NickName 一开始可以没有
NickName string `gorm:"type:varchar(20)"`
Birthday *time.Time `gorm:"type:datetime"`
Gender string `gorm:"column:gender;default:male;type:varchar(6) comment 'female表示女, male表示男'"`
// Role 用户权限身份 1-普通 2-管理员
Role int `gorm:"column:role;default:1;type:int comment '1-普通 2-管理员'"`
}
1-2同步表结构
//user_srv/model/main/main.go
package main
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"gorm.io/gorm/schema"
"log"
"mxshop_srvs/user_srv/model"
"os"
"time"
)
func main() {
dsn := "root:123456@tcp(127.0.0.1:3307)/mxshop_user_srv?charset=utf8mb4&parseTime=True&loc=Local"
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
logger.Config{
SlowThreshold: time.Second, // 慢 SQL 阈值
LogLevel: logger.Info,
Colorful: true, // 禁用彩色打印
},
)
// 全局模式
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
NamingStrategy: schema.NamingStrategy{
// 为false会根据 结构体名+s 作为表名
SingularTable: true,
},
Logger: newLogger,
})
if err != nil {
panic(err)
}
// 定义一个表结构, 生成对应表
_ = db.AutoMigrate(&model.User{})
}
1-3md5加密
// user_srv/model/main/main.go
package main
// genMd5 将传入的字符串进行md5加密
func genMd5(code string) string {
Md5 := md5.New()
_, _ = io.WriteString(Md5, code)
return hex.EncodeToString(Md5.Sum(nil))
}
// ...
1-4md5盐值加密解决用户密码安全问题
go get github.com/anaskhan96/go-password-encoder
func Test_GoPasswordEncoder(t *testing.T) {
// Using custom options
options := &password.Options{16, 100, 32, sha512.New}
salt, encodedPwd := password.Encode("generic password", options)
println(salt)
println(encodedPwd)
newPassword := fmt.Sprintf("$pbkdf2-sha512$%s$%s", salt, encodedPwd)
println(len(newPassword))
println(newPassword)
// 从数据库中拿出
passwordInfo := strings.Split(newPassword, "$")
fmt.Println(passwordInfo)
check := password.Verify("generic password", passwordInfo[2], passwordInfo[3], options)
fmt.Println(check) // true
}
1-5定义proto接口
//user_srv/proto/user.proto
syntax = "proto3";
import "google/protobuf/empty.proto";
option go_package = ".;proto";
// 做成通用接口, 减少业务功能的耦合
service User{
rpc GetUserList(PageInfo)returns (UserListResponse); // 用户列表
rpc GetUserMobile(MobileRequest) returns (UserInfoResponse); // 通过mobile查询用户
rpc GetUserId(IdRequest) returns (UserInfoResponse); // 通过id查询用户
rpc CreateUser(CreateUserInfo) returns (UserInfoResponse); // 创建用户
rpc UpdateUser(UpdateUserInfo)returns (google.protobuf.Empty); // 更新用户
rpc CheckPassword(PasswordCheckInfo) returns (CheckResponse); // 检查密码
}
message PasswordCheckInfo{
string password = 1;
string encryptedPassword = 2;
}
message CheckResponse{
bool success = 1;
}
message PageInfo {
uint32 pn = 1;
uint32 pSize = 2;
}
message MobileRequest{
string mobile = 1;
}
message IdRequest{
int32 id = 1;
}
message CreateUserInfo{
string nickName = 1;
string password = 2;
string mobile = 3;
}
message UpdateUserInfo{
int32 id = 1;
string nickName = 2;
string gender = 3;
uint64 birthDay = 4;
}
message UserInfoResponse{
int32 id = 1;
string password = 2;
string mobile = 3;
string nickName = 4;
uint64 birthDay = 5;
string gender = 6;
int32 role = 7;
}
message UserListResponse{
int32 total = 1;
repeated UserInfoResponse data = 2;
}
1-6用户列表接口
# protoc -I . user.proto --go_out=plugins=grpc:.
# --go_out: protoc-gen-go: plugins are not supported; use 'protoc --go-grpc_out=...' to generate gRPC
protoc --go_out=. --go-grpc_out=. user.proto
视频里只生成一个,我生成了两个(可能是因为新版)
// user_srv/handler/user.go
package handler
import (
"context"
"google.golang.org/grpc"
"gorm.io/gorm"
"mxshop_srvs/user_srv/global"
"mxshop_srvs/user_srv/model"
"mxshop_srvs/user_srv/proto"
)
type UserServer struct{}
// ModelToResponse user对象转为响应对象
func ModelToResponse(user model.User) proto.UserInfoResponse {
// grpc的message中字段有默认值, 不能随便赋值nil, 容易出错
userInfoRsp := proto.UserInfoResponse{
Id: user.ID,
Password: user.Password,
NickName: user.NickName,
Gender: user.Gender,
Role: int32(user.Role),
}
if user.Birthday != nil {
userInfoRsp.BirthDay = uint64(user.Birthday.Unix())
}
return userInfoRsp
}
// Paginate 优雅分页
func Paginate(page, pageSize int) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
if page <= 0 {
page = 1
}
switch {
case pageSize > 100:
pageSize = 100
case pageSize <= 0:
pageSize = 10
}
offset := (page - 1) * pageSize
return db.Offset(offset).Limit(pageSize)
}
}
// GetUserList 获取用户列表
func (s *UserServer) GetUserList(
ctx context.Context,
req *proto.PageInfo) (
*proto.UserListResponse, error,
) {
var users []model.User
result := global.DB.Find(&users)
if result.Error != nil {
return nil, result.Error
}
rsp := &proto.UserListResponse{}
// 总共多少条数据
rsp.Total = int32(result.RowsAffected)
// 进行分页
global.DB.Scopes(Paginate(int(req.Pn), int(req.PSize))).Find(&users)
// 转换users为rsp对象
for _, user := range users {
userInfoRsp := ModelToResponse(user)
rsp.Data = append(rsp.Data, &userInfoRsp)
}
return rsp, nil
}
//user_srv/global/global.go
package global
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"gorm.io/gorm/schema"
"log"
"mxshop_srvs/user_srv/model"
"os"
"time"
)
var (
DB *gorm.DB
)
// init go中import某个包会自动执行该包的init方法
func init() {
dsn := "root:123456@tcp(127.0.0.1:3307)/mxshop_user_srv?charset=utf8mb4&parseTime=True&loc=Local"
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
logger.Config{
SlowThreshold: time.Second, // 慢 SQL 阈值
LogLevel: logger.Info,
Colorful: true, // 禁用彩色打印
},
)
// 全局模式
var err error
DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
NamingStrategy: schema.NamingStrategy{
// 为false会根据 结构体名+s 作为表名
SingularTable: true,
},
Logger: newLogger,
})
if err != nil {
panic(err)
}
// 定义一个表结构, 生成对应表
_ = DB.AutoMigrate(&model.User{})
}
1-7通过id和mobile查询用户
// user_srv/handler/user.go
// ...
// GetUserMobile 通过手机号查询用户
func (s *UserServer) GetUserMobile(ctx context.Context, req *proto.MobileRequest) (*proto.UserInfoResponse, error) {
var user model.User
result := global.DB.Where(&model.User{Mobile: req.Mobile}).First(&user)
if result.RowsAffected == 0 {
return nil, status.Error(codes.NotFound, "用户不存在")
}
if result.Error != nil {
return nil, result.Error
}
userInfoRsp := ModelToResponse(user)
return &userInfoRsp, nil
}
// GetUserId 通过id查询用户
func (s *UserServer) GetUserId(ctx context.Context, req *proto.IdRequest) (*proto.UserInfoResponse, error) {
var user model.User
result := global.DB.First(&user,req.Id)
if result.RowsAffected == 0 {
return nil, status.Error(codes.NotFound, "用户不存在")
}
if result.Error != nil {
return nil, result.Error
}
userInfoRsp := ModelToResponse(user)
return &userInfoRsp, nil
}
1-8新建用户
// user_srv/handler/user.go
// ...
// CreateUser 新建用户
func (s *UserServer) CreateUser(ctx context.Context, info *proto.CreateUserInfo) (*proto.UserInfoResponse, error) {
var user model.User
result := global.DB.Where(&model.User{Mobile: info.Mobile}).First(&user)
if result.RowsAffected == 1 {
return nil, status.Error(codes.AlreadyExists, "用户已存在")
}
user.Mobile = info.Mobile
user.NickName = info.NickName
// 密码加盐
options := &password.Options{16, 100, 32, sha512.New}
salt, encodedPwd := password.Encode(info.Password, options)
user.Password = fmt.Sprintf("$pbkdf2-sha512$%s$%s", salt, encodedPwd)
// 创建成功会填充id
result = global.DB.Create(&user)
if result.Error != nil {
return nil, status.Errorf(codes.Internal, result.Error.Error())
}
userInfoRsp := ModelToResponse(user)
return &userInfoRsp, nil
}
1-9修改用户和校验密码接口
// user_srv/handler/user.go
//...
// UpdateUser 更新用户信息
func (s *UserServer) UpdateUser(ctx context.Context, info *proto.UpdateUserInfo) (*emptypb.Empty, error) {
var user model.User
result := global.DB.First(&user, info.Id)
if result.RowsAffected == 0 {
return nil, status.Error(codes.NotFound, "用户不存在")
}
// 时间转换
birthDay := time.Unix(int64(info.BirthDay), 0)
user.NickName = info.NickName
user.Birthday = &birthDay
user.Gender = info.Gender
result = global.DB.Save(user)
if result.Error != nil {
return nil, status.Errorf(codes.Internal, result.Error.Error())
}
return &emptypb.Empty{}, nil
}
// CheckPassword 校验密码
func (s *UserServer) CheckPassword(ctx context.Context, info *proto.PasswordCheckInfo) (*proto.CheckResponse, error) {
passwordInfo := strings.Split(info.EncryptedPassword, "$")
options := &password.Options{16, 100, 32, sha512.New}
check := password.Verify(info.Password, passwordInfo[2], passwordInfo[3], options)
return &proto.CheckResponse{Success: check}, nil
}
1-10通过flag启动grpc服务
// user_srv/handler/user.go
// ...
type UserServer struct {
// ctrl+i快速实现接口
*proto.UnimplementedUserServer
}
func (s *UserServer) mustEmbedUnimplementedUserServer() {
}
// user_srv/main.go
package main
import (
"flag"
"fmt"
"google.golang.org/grpc"
"mxshop_srvs/user_srv/handler"
"mxshop_srvs/user_srv/proto"
"net"
)
func main() {
// 让用户从命令行传递
IP := flag.String("ip", "0.0.0.0", "ip地址")
Port := flag.Int("port", 50051, "端口号")
flag.Parse()
fmt.Println("ip: ", *IP)
fmt.Println("port: ", *Port)
server := grpc.NewServer()
proto.RegisterUserServer(server, &handler.UserServer{})
lis, err := net.Listen("tcp", fmt.Sprintf("%s:%d", *IP, *Port))
if err != nil {
panic("failed to listen: " + err.Error())
}
err = server.Serve(lis)
if err != nil {
panic("failed to start grpc: " + err.Error())
}
}
1-12测试用户微服务接口
//user_srv/tests/user.go
package main
import (
"context"
"fmt"
"google.golang.org/grpc"
"mxshop_srvs/user_srv/proto"
)
var userClient proto.UserClient
var conn *grpc.ClientConn
func Init() {
var err error
conn, err = grpc.Dial("127.0.0.1:50051", grpc.WithInsecure())
if err != nil {
panic(err)
}
userClient = proto.NewUserClient(conn)
}
func TestGetUserList() {
rsp, err := userClient.GetUserList(context.Background(), &proto.PageInfo{
Pn: 1,
PSize: 2,
})
if err != nil {
panic(err)
}
for _, user := range rsp.Data {
fmt.Println(user.Mobile, user.NickName, user.Password)
checkRsp, err := userClient.CheckPassword(context.Background(), &proto.PasswordCheckInfo{
Password: "admin123",
EncryptedPassword: user.Password,
})
if err != nil {
panic(err)
}
fmt.Println(checkRsp)
}
}
func TestCreateUser() {
for i := 0; i < 10; i++ {
rsp, err := userClient.CreateUser(context.Background(), &proto.CreateUserInfo{
NickName: fmt.Sprintf("boddy22%d", i),
Mobile: fmt.Sprintf("1878222411%d", i),
Password: "admin123",
})
if err != nil {
panic(err)
}
fmt.Println(rsp.Id)
}
}
func main() {
Init()
//TestCreateUser()
TestGetUserList()
conn.Close()
}
1-13课后作业
第09周 用户服务的web服务
第1章web层开发-基础项目架构
1-1新建项目和目录结构构建
1-2go高性能日志库-zap使用
go get -u go.uber.org/zap
//user-web/zap-test/main.go
package main
import "go.uber.org/zap"
func main() {
// 生产环境
logger, _ := zap.NewProduction()
//logger, _ := zap.NewDevelopment()
defer logger.Sync()
url := "https://imooc.com"
// sugar用于简化api操作
sugar := logger.Sugar()
sugar.Infow("failed to fetch URL",
"url", url,
"attempt", 3)
sugar.Infof("Failed to fetch URL: %s", url)
}
1-3zap的文件输出
//user-web/zap-test/zap-log-file/main.go
package main
import "go.uber.org/zap"
func NewLogger() (*zap.Logger, error) {
cfg := zap.NewProductionConfig()
// 当前main.go所在目录下build, 然后运行build才是生成在./mypro.log
// 直接run是到项目根目录
//cfg.OutputPaths = []string{"./mypro.log"}
cfg.OutputPaths = []string{"./mypro.log", "stderr", "stdout"}
return cfg.Build()
}
func main() {
logger, err := NewLogger()
if err != nil {
panic(err)
}
su := logger.Sugar()
defer su.Sync()
url := "https://imooc.com"
su.Infow("failed to fetch URL",
"url", url,
"attempt", 3)
su.Infof("Failed to fetch URL: %s", url)
}
1-4集成zap和理由初始到gin的启动过程
// user-web/main.go
package main
import (
"fmt"
"go.uber.org/zap"
"mxshop-api/user-web/initialize"
)
func main() {
port := 8021
// 1. 初始化logger
initialize.InitLogger()
// 2. 初始化router
r := initialize.Routers()
// 3. 运行接口
// zap.S() 直接拿到zap的suger
// S()和L()有加锁,并且使用的是全局logger,可以全局安全访问
zap.S().Infof("启动服务器, 端口: %d", port)
err := r.Run(fmt.Sprintf(":%d", port))
if err != nil {
zap.S().Panic("启动失败: ", err.Error())
}
}
//user-web/initialize/logger.go
package initialize
import "go.uber.org/zap"
func InitLogger() {
// 日志分级别(debug,info,warn,err,fetal), production打印info及以上级别
//logger, _ := zap.NewProduction()
logger, _ := zap.NewDevelopment() // dev日志不是json格式
// zap.S() 里的logger对象是没有配置打印的, 我们要自己设置全局logger
zap.ReplaceGlobals(logger)
}
// user-web/initialize/router.go
package initialize
import (
"github.com/gin-gonic/gin"
"mxshop-api/user-web/router"
)
func Routers() *gin.Engine {
r := gin.Default()
apiGroup := r.Group("/v1")
// 分组注册接口
router.InitUserRouter(apiGroup)
return r
}
//user-web/api/user.go
package api
import (
"github.com/gin-gonic/gin"
)
func GetUserList(ctx *gin.Context) {
}
// user-web/router/user.go
package router
import (
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"mxshop-api/user-web/api"
)
func InitUserRouter(group *gin.RouterGroup) {
userRouter := group.Group("user")
zap.S().Info("配置用户相关的url")
{
userRouter.GET("list", api.GetUserList)
}
}
1-6gin调用grpc服务
复制user_srv的proto来生成grpc接口
protoc --go_out=. --go-grpc_out=. user.proto
// user-web/api/user.go
package api
import (
"context"
"fmt"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"mxshop-api/user-web/global/response"
"mxshop-api/user-web/proto"
"net/http"
"time"
)
// HandleGrpcErrorToHttp grpc的code转换为http状态码
func HandleGrpcErrorToHttp(err error, c *gin.Context) {
if err != nil {
if e, ok := status.FromError(err); ok {
switch e.Code() {
case codes.NotFound:
c.JSON(http.StatusNotFound, gin.H{
"msg": e.Message(),
})
case codes.Internal:
c.JSON(http.StatusInternalServerError, gin.H{
"msg": "内部错误",
})
case codes.InvalidArgument:
c.JSON(http.StatusBadRequest, gin.H{
"msg": "参数错误",
})
default:
c.JSON(http.StatusInternalServerError, gin.H{
//"msg": "其他错误: "+e.Message(),
"msg": "其他错误",
})
}
return
}
}
}
func GetUserList(ctx *gin.Context) {
zap.S().Debug("[GetUserList] 获取 [用户列表]")
ip := "127.0.0.1"
port := 50051
// 连接user grpc服务
userConn, err := grpc.Dial(fmt.Sprintf("%s:%d", ip, port), grpc.WithInsecure())
if err != nil {
zap.S().Errorw("[GetUserList] 连接 [用户服务失败] ", "msg", err.Error())
return
}
// 创建client
client := proto.NewUserClient(userConn)
// 调用接口
rsp, err := client.GetUserList(context.Background(), &proto.PageInfo{
Pn: 0,
PSize: 0,
})
if err != nil {
zap.S().Errorw("[GetUserList] 查询 [用户列表] 失败", "msg", err.Error())
HandleGrpcErrorToHttp(err, ctx)
return
}
res := make([]interface{}, 0)
for _, value := range rsp.Data {
//data := make(map[string]interface{})
//data["id"] = value.Id
//data["name"] = value.NickName
//data["birthday"] = value.BirthDay
//data["gender"] = value.Gender
//data["mobile"] = value.Mobile
//res = append(res, data)
user := response.UserResponse{
Id: value.Id,
NickName: value.NickName,
//Birthday: time.Unix(int64(value.BirthDay), 0) ,
//Birthday: time.Unix(int64(value.BirthDay), 0).Format("2006-01-02"),
Birthday: response.JsonTime(time.Unix(int64(value.BirthDay), 0)),
Gender: value.Gender,
Mobile: value.Mobile,
}
res = append(res, user)
}
ctx.JSON(http.StatusOK, res)
}
// user-web/global/response/user.go
package response
import (
"fmt"
"time"
)
type JsonTime time.Time
func (j JsonTime) MarshalJSON() ([]byte, error) {
var stmp = fmt.Sprintf("\"%s\"", time.Time(j).Format("2006-01-02"))
return []byte(stmp), nil
}
type UserResponse struct {
Id int32 `json:"id"`
NickName string `json:"name"`
Birthday JsonTime `json:"birthday"`
//Birthday string `json:"birthday"`
Gender string `json:"gender"`
Mobile string `json:"mobile"`
}
1-8配置文件-viper
// user-web/test/viper_test/main.go
package main
import (
"fmt"
"github.com/spf13/viper"
)
// 映射为struct
type ServerConfig struct {
ServiceName string `mapstructure:"name"`
Port int `mapstructure:"port"`
}
func main() {
v := viper.New()
// go文件路径问题
// 当前目录 go build 后运行可以成功拿到config.yaml
//v.SetConfigFile("config.yaml")
// 快捷得到该路径: 右键 -> 来自内容根的路径;
// 作用原理: goland工作目录 D:/go/projs/imooc/mxshop-api -> 相对路径 user-web/test/viper_test/config.yaml
// build后要在项目根目录运行 D:\go\projs\imooc\mxshop-api> .\user-web\test\viper_test\main.exe
v.SetConfigFile("user-web/test/viper_test/config.yaml")
err := v.ReadInConfig()
if err != nil {
panic(err)
}
fmt.Println(v.Get("name"))
sc := ServerConfig{}
err = v.Unmarshal(&sc)
if err != nil {
panic(err)
}
fmt.Println(sc)
}
# user-web/test/viper_test
name: 'user-web'
port: 8021
1-9viper的配置环境开发环境和生产环境隔离
// user-web/test/viper_test/ch02/main.go
package main
import (
"fmt"
"github.com/fsnotify/fsnotify"
"github.com/spf13/viper"
"time"
)
type MysqlConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
}
// ServerConfig yaml映射为struct
type ServerConfig struct {
// 要大写, 否则反射无法set
ServiceName string `mapstructure:"name"`
MysqlInfo MysqlConfig `mapstructure:"mysql"`
}
func GetEnvInfo(env string) bool {
viper.AutomaticEnv()
// 设置的环境变量要生效, 要重启ide(不要用缓存清除重启)
return viper.GetBool(env)
}
func main() {
debug := GetEnvInfo("MXSHOP_DEBUG")
configFilePrefix := "config"
configFileName := fmt.Sprintf("user-web/test/viper_test/ch02/%s-product.yaml", configFilePrefix)
// 线上线下配置隔离
if debug {
configFileName = fmt.Sprintf("user-web/test/viper_test/ch02/%s-debug.yaml", configFilePrefix)
}
v := viper.New()
// go文件路径问题
// 当前目录 go build 后运行可以成功拿到config.yaml
//v.SetConfigFile("config.yaml")
// 快捷得到该路径: 右键 -> 来自内容根的路径;
// 作用原理: goland工作目录 D:/go/projs/imooc/mxshop-api -> 相对路径 user-web/test/viper_test/config.yaml
// build后要在项目根目录运行 D:\go\projs\imooc\mxshop-api> .\user-web\test\viper_test\main.exe
v.SetConfigFile(configFileName)
err := v.ReadInConfig()
var sc ServerConfig
err = v.Unmarshal(&sc)
if err != nil {
panic(err)
}
fmt.Println(sc)
// viper动态监听变化(试了没用)
v.WriteConfig()
v.OnConfigChange(func(e fsnotify.Event) {
fmt.Println("config file changed: " + e.Name)
_ = v.ReadInConfig()
_ = v.Unmarshal(&sc)
fmt.Println(sc)
})
time.Sleep(300 * time.Second)
}
mysql:
host: "127.0.0.1"
port: 3307
name: user-web
1-10viper集成到gin的web服务中
// user-web/initialize/config.go
package initialize
import (
"fmt"
"github.com/fsnotify/fsnotify"
"github.com/spf13/viper"
"go.uber.org/zap"
"mxshop-api/user-web/global"
)
func GetEnvInfo(env string) bool {
viper.AutomaticEnv()
// 设置的环境变量要生效, 要重启ide(不要用缓存清除重启)
return viper.GetBool(env)
}
func InitConfig() {
debug := GetEnvInfo("MXSHOP_DEBUG")
configFilePrefix := "config"
configFileName := fmt.Sprintf("user-web/%s-product.yaml", configFilePrefix)
// 线上线下配置隔离
if debug {
configFileName = fmt.Sprintf("user-web/%s-debug.yaml", configFilePrefix)
}
v := viper.New()
v.SetConfigFile(configFileName)
err := v.ReadInConfig()
zap.S().Infof("配置消息: %&v", global.ServerConfig)
//var sc config.ServerConfig
err = v.Unmarshal(&global.ServerConfig)
if err != nil {
panic(err)
}
// viper动态监听变化(试了没用)
v.WriteConfig()
v.OnConfigChange(func(e fsnotify.Event) {
fmt.Println("config file changed: " + e.Name)
zap.S().Infof("配置文件产生变化: %s", e.Name)
_ = v.ReadInConfig()
_ = v.Unmarshal(&global.ServerConfig)
zap.S().Infof("配置消息: %v", global.ServerConfig)
})
}
// user-web/config/config.go
package config
type UserSrvConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
}
type ServerConfig struct {
Name string `mapstructure:"name"`
Port int `mapstructure:"port"`
UserSrvInfo UserSrvConfig `mapstructure:"user_srv"`
}
// user-web/api/user.go
package api
// ...
func GetUserList(ctx *gin.Context) {
zap.S().Debug("[GetUserList] 获取 [用户列表]")
//ip := "127.0.0.1"
//port := 50051
info := global.ServerConfig.UserSrvInfo
// 连接user grpc服务
userConn, err := grpc.Dial(fmt.Sprintf("%s:%d", info.Host, info.Port), grpc.WithInsecure())
if err != nil {
zap.S().Errorw("[GetUserList] 连接 [用户服务失败] ", "msg", err.Error())
return
}
// ...
}
// user-web/main.go
package main
import (
"fmt"
"go.uber.org/zap"
"mxshop-api/user-web/global"
"mxshop-api/user-web/initialize"
)
func main() {
// 1. 初始化logger
initialize.InitLogger()
// 初始化配置
initialize.InitConfig()
// 2. 初始化router
r := initialize.Routers()
// 3. 运行接口
// zap.S() 直接拿到zap的suger
// S()和L()有加锁,并且使用的是全局logger,可以全局安全访问
zap.S().Infof("启动服务器, 端口: %d", global.ServerConfig.Port)
err := r.Run(fmt.Sprintf(":%d", global.ServerConfig.Port))
if err != nil {
zap.S().Panic("启动失败: ", err.Error())
}
}
name: user-web
port: 8021
user_srv:
host: 127.0.0.1
port: 50051
// user-web/global/global.go
package global
import "mxshop-api/user-web/config"
var (
ServerConfig *config.ServerConfig = &config.ServerConfig{}
)
第2章web层开发-用户接口开发
2-1表单验证的初始化
// user-web/api/user.go
package api
func removeTopStruct(fields map[string]string) map[string]string {
rsp := map[string]string{}
for field, err := range fields {
rsp[field[strings.Index(field, ".")+1:]] = err
}
return rsp
}
// ...
// PasswordLogin 登录
func PasswordLogin(c *gin.Context) {
// 表单验证
passwordLoginForm := forms.PasswordLoginForm{}
if err := c.ShouldBind(&passwordLoginForm); err != nil {
// 如何返回错误信息
errs, ok := err.(validator.ValidationErrors)
if !ok {
c.JSON(http.StatusOK, gin.H{
"msg": err.Error(),
})
}
c.JSON(http.StatusBadRequest, gin.H{
"error": removeTopStruct(errs.Translate(global.Trans)),
})
return
}
}
// user-web/router/user.go
package router
import (
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"mxshop-api/user-web/api"
)
func InitUserRouter(group *gin.RouterGroup) {
userRouter := group.Group("user")
zap.S().Info("配置用户相关的url")
{
userRouter.GET("list", api.GetUserList)
userRouter.POST("pwd_login", api.PasswordLogin)
}
}
// user-web/initialize/Validator.go
package initialize
import (
"fmt"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/locales/en"
"github.com/go-playground/locales/zh"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
en_translations "github.com/go-playground/validator/v10/translations/en"
zh_translations "github.com/go-playground/validator/v10/translations/zh"
"mxshop-api/user-web/global"
"reflect"
"strings"
)
func InitTrans(locale string) (err error) {
// 修改gin的validator引擎属性,实现定制
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
// 注册一个获取json的tag的自定义方法
v.RegisterTagNameFunc(func(fld reflect.StructField) string {
name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
if name == "-" {
return ""
}
return name
})
zhT := zh.New() // 中文翻译器
enT := en.New() // 英文翻译器
// 第一个参数是备用的语言环境, 后面的参数是应支持的语言环境
uni := ut.New(enT, zhT, enT)
global.Trans, ok = uni.GetTranslator(locale)
if !ok {
return fmt.Errorf("uni.GetTranslator(%s)", locale)
}
switch locale {
case "en":
en_translations.RegisterDefaultTranslations(v, global.Trans)
case "zh":
zh_translations.RegisterDefaultTranslations(v, global.Trans)
default:
en_translations.RegisterDefaultTranslations(v, global.Trans)
}
return
}
return
}
// user-web/main.go
package main
func main() {
// ...
// 2. 初始化router
r := initialize.Routers()
// 初始化翻译
if err := initialize.InitTrans("zh"); err != nil {
panic(err)
}
// ...
}
// user-web/global/global.go
package global
import (
ut "github.com/go-playground/universal-translator"
"mxshop-api/user-web/config"
)
var (
ServerConfig *config.ServerConfig = &config.ServerConfig{}
Trans ut.Translator
)
// user-web/forms/user.go
package forms
type PasswordLoginForm struct {
// 手机号码的格式如何校验? -> 自定义validate
Mobile string `form:"mobile" json:"mobile" binding:"required"`
Password string `form:"password" json:"password" binding:"required,min=3,max=20"`
}
2-2自定义mobile验证器
// user-web/main.go
package main
func main() {
// ...
// 初始化翻译
if err := initialize.InitTrans("zh"); err != nil {
panic(err)
}
// 注册验证器 自定义validator
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
_ = v.RegisterValidation("mobile", myvalidator.ValidateMobile)
// 自定义validator的翻译
_ = v.RegisterTranslation("mobile", global.Trans, func(ut ut.Translator) error {
return ut.Add("mobile", "{0} 非法的手机号码!", true)
}, func(ut ut.Translator, fe validator.FieldError) string {
t, _ := ut.T("mobile", fe.Field())
return t
})
}
// 3. 运行接口
// ...
}
// user-web/validator/validators.go
package validator
import (
"github.com/go-playground/validator/v10"
"regexp"
)
func ValidateMobile(fl validator.FieldLevel) bool {
mobile := fl.Field().String()
// 使用正则表达式判断是否合法
ok, _ := regexp.MatchString(`^1([38][0-9]|14[579]|5[^4]|16[6]|7[1-35-8]|9[189])\d{8}$`, mobile)
if !ok {
return false
}
return true
}
// user-web/forms/user.go
package forms
type PasswordLoginForm struct {
// 手机号码的格式如何校验? -> 自定义validate
Mobile string `form:"mobile" json:"mobile" binding:"required,mobile"`
Password string `form:"password" json:"password" binding:"required,min=3,max=20"`
}
2-3登录逻辑完善
// user-web/api/user.go
package api
// ...
// PasswordLogin 登录
func PasswordLogin(c *gin.Context) {
// 表单验证
passwordLoginForm := forms.PasswordLoginForm{}
if err := c.ShouldBind(&passwordLoginForm); err != nil {
// 如何返回错误信息
errs, ok := err.(validator.ValidationErrors)
if !ok {
c.JSON(http.StatusOK, gin.H{
"msg": err.Error(),
})
}
c.JSON(http.StatusBadRequest, gin.H{
"error": removeTopStruct(errs.Translate(global.Trans)),
})
return
}
info := global.ServerConfig.UserSrvInfo
userConn, err := grpc.Dial(fmt.Sprintf("%s:%d", info.Host, info.Port), grpc.WithInsecure())
if err != nil {
zap.S().Errorw("[PasswordLogin] 连接 [用户服务失败] ", "msg", err.Error())
return
}
client := proto.NewUserClient(userConn)
// 登录
if user, err := client.GetUserMobile(context.Background(), &proto.MobileRequest{
Mobile: passwordLoginForm.Mobile,
}); err != nil {
if e, ok := status.FromError(err); ok {
switch e.Code() {
case codes.NotFound:
c.JSON(http.StatusBadRequest, map[string]string{
"mobile": "用户不存在",
})
default:
c.JSON(http.StatusInternalServerError, map[string]string{
"mobile": "登录失败",
})
}
return
}
} else {
// 查询到了用户, 没有检查密码
if rsp, err := client.CheckPassword(context.Background(), &proto.PasswordCheckInfo{
Password: passwordLoginForm.Password,
EncryptedPassword: user.Password,
}); err != nil {
c.JSON(http.StatusInternalServerError, map[string]string{
"password": "登录失败",
})
} else {
if rsp.Success {
c.JSON(http.StatusOK, map[string]string{
"msg": "登录成功",
})
} else {
c.JSON(http.StatusBadRequest, map[string]string{
"password": "密码错误",
})
}
}
}
}
2-4session机制在微服务下的问题
2-5jsonwebtoken的认证机制
2-6集成jwt到gin中
// user-web/api/user.go
// ...
// PasswordLogin 登录
func PasswordLogin(c *gin.Context) {
// ...
// 登录
if user, err := client.GetUserMobile(context.Background(), &proto.MobileRequest{
Mobile: passwordLoginForm.Mobile,
}); err != nil {
if e, ok := status.FromError(err); ok {
// ...
}
} else {
// 查询到了用户, 没有检查密码
if rsp, err := client.CheckPassword(context.Background(), &proto.PasswordCheckInfo{
Password: passwordLoginForm.Password,
EncryptedPassword: user.Password,
}); err != nil {
c.JSON(http.StatusInternalServerError, map[string]string{
"password": "登录失败",
})
} else {
if rsp.Success {
// 生成 jwt token
j := middlewares.NewJWT()
claims := models.CustomClaims{
ID: uint(user.Id),
NickName: user.NickName,
AuthorityId: uint(user.Role),
StandardClaims: jwt.StandardClaims{
Audience: "",
// 30天过期
ExpiresAt: time.Now().Unix() + 60*60*24*30,
// 哪个机构认证的
Issuer: "malred",
// 签名生效时间
NotBefore: time.Now().Unix(),
},
}
token, err := j.CreateToken(claims)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"msg": "生成token失败",
})
}
c.JSON(http.StatusOK, gin.H{
"id": user.Id,
"nick_name": user.NickName,
"token": token,
"expired_at": (time.Now().Unix() + 60*60*24*30) * 1000,
"msg": "登录成功",
})
} else {
c.JSON(http.StatusBadRequest, map[string]string{
"password": "密码错误",
})
}
}
}
}
jwt:
key: 3hTQH29YPr5oCcwatf1a1KbcnAft8Hn1
name: user-web
port: 8021
user_srv:
host: 127.0.0.1
port: 50051
// user-web/config/config.go
package config
type UserSrvConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
}
type JWTConfig struct {
SigningKey string `mapstructure:"key"`
}
type ServerConfig struct {
Name string `mapstructure:"name"`
Port int `mapstructure:"port"`
UserSrvInfo UserSrvConfig `mapstructure:"user_srv"`
JWTInfo JWTConfig `mapstructure:"jwt"`
}
// user-web/middlewares/jwt.go
package middlewares
import (
"errors"
"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
"mxshop-api/user-web/global"
"mxshop-api/user-web/models"
"net/http"
"time"
)
func JWTAuth() gin.HandlerFunc {
return func(c *gin.Context) {
// 我们这里jwt鉴权取头部信息 x-token 登录时回返回token信息 这里前端需要把token存储到cookie或者本地localSstorage中 不过需要跟后端协商过期时间 可以约定刷新令牌或者重新登录
token := c.Request.Header.Get("x-token")
if token == "" {
c.JSON(http.StatusUnauthorized, map[string]string{
"msg": "请登录",
})
c.Abort()
return
}
j := NewJWT()
// parseToken 解析token包含的信息
claims, err := j.ParseToken(token)
if err != nil {
if err == TokenExpired {
if err == TokenExpired {
c.JSON(http.StatusUnauthorized, map[string]string{
"msg": "授权已过期",
})
c.Abort()
return
}
}
c.JSON(http.StatusUnauthorized, "未登陆")
c.Abort()
return
}
c.Set("claims", claims)
c.Set("userId", claims.ID)
c.Next()
}
}
type JWT struct {
SigningKey []byte
}
var (
TokenExpired = errors.New("Token is expired")
TokenNotValidYet = errors.New("Token not active yet")
TokenMalformed = errors.New("That's not even a token")
TokenInvalid = errors.New("Couldn't handle this token")
)
func NewJWT() *JWT {
return &JWT{
[]byte(global.ServerConfig.JWTInfo.SigningKey), //可以设置过期时间
}
}
// 创建一个token
func (j *JWT) CreateToken(claims models.CustomClaims) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(j.SigningKey)
}
// 解析 token
func (j *JWT) ParseToken(tokenString string) (*models.CustomClaims, error) {
token, err := jwt.ParseWithClaims(tokenString, &models.CustomClaims{}, func(token *jwt.Token) (i interface{}, e error) {
return j.SigningKey, nil
})
if err != nil {
if ve, ok := err.(*jwt.ValidationError); ok {
if ve.Errors&jwt.ValidationErrorMalformed != 0 {
return nil, TokenMalformed
} else if ve.Errors&jwt.ValidationErrorExpired != 0 {
// Token is expired
return nil, TokenExpired
} else if ve.Errors&jwt.ValidationErrorNotValidYet != 0 {
return nil, TokenNotValidYet
} else {
return nil, TokenInvalid
}
}
}
if token != nil {
if claims, ok := token.Claims.(*models.CustomClaims); ok && token.Valid {
return claims, nil
}
return nil, TokenInvalid
} else {
return nil, TokenInvalid
}
}
// 更新token
func (j *JWT) RefreshToken(tokenString string) (string, error) {
jwt.TimeFunc = func() time.Time {
return time.Unix(0, 0)
}
token, err := jwt.ParseWithClaims(tokenString, &models.CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
return j.SigningKey, nil
})
if err != nil {
return "", err
}
if claims, ok := token.Claims.(*models.CustomClaims); ok && token.Valid {
jwt.TimeFunc = time.Now
claims.StandardClaims.ExpiresAt = time.Now().Add(1 * time.Hour).Unix()
return j.CreateToken(*claims)
}
return "", TokenInvalid
}
// user-web/models/request.go
package models
import "github.com/dgrijalva/jwt-go"
type CustomClaims struct {
ID uint
NickName string
AuthorityId uint
jwt.StandardClaims
}
2-7给url添加登录权限验证
// user-web/middlewares/admin.go
package middlewares
import (
"github.com/gin-gonic/gin"
"mxshop-api/user-web/models"
"net/http"
)
func IsAdminAuth() gin.HandlerFunc {
return func(c *gin.Context) {
claims, _ := c.Get("claims")
currentUser := claims.(*models.CustomClaims)
// 管理员
if currentUser.AuthorityId != 2 {
c.JSON(http.StatusForbidden, gin.H{
"msg": "无权限",
})
c.Abort()
return
} else {
c.Next()
}
}
}
// user-web/api/user.go
// ...
// GetUserList 获取用户列表 - 分页
func GetUserList(ctx *gin.Context) {
// ...
// 连接user grpc服务
userConn, err := grpc.Dial(fmt.Sprintf("%s:%d", info.Host, info.Port), grpc.WithInsecure())
if err != nil {
zap.S().Errorw("[GetUserList] 连接 [用户服务失败] ", "msg", err.Error())
return
}
claims, _ := ctx.Get("claims")
currentUser := claims.(*models.CustomClaims)
zap.S().Infof("访问用户: %d", currentUser.ID)
// ...
}
// user-web/router/user.go
package router
import (
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"mxshop-api/user-web/api"
"mxshop-api/user-web/middlewares"
)
func InitUserRouter(group *gin.RouterGroup) {
userRouter := group.Group("user")
zap.S().Info("配置用户相关的url")
{
userRouter.GET("list", middlewares.JWTAuth(),middlewares.IsAdminAuth(), api.GetUserList)
userRouter.POST("pwd_login", api.PasswordLogin)
}
}
2-8如何解决前后端的跨域问题
<!--user-web/index.html-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
</head>
<body>
<button type="button" id="query">get</button>
<div id="content" style="background-color: aquamarine;width: 300px;height: 500px;"></div>
<script type="text/javascript">
$("#query").click(function () {
$.ajax(
{
url: 'http://127.0.0.1:8021/v1/user/list',
dataType: "json",
type: "get",
beforeSend: function (req) {
req.setRequestHeader("x-token",
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJJRCI6MTYsIk5pY2tOYW1lIjoiYm9kZHkxNCIsIkF1dGhvcml0eUlkIjoxLCJleHAiOjE3MDA2OTI0MDQsImlzcyI6Im1hbHJlZCIsIm5iZiI6MTY5ODEwMDQwNH0.iG9o1pGYUh0JyiAHGvm-kv-TcFg6WkAgqzfZ66y1cW8")
},
success: function (res) {
console.log(res)
$('#content').text(res.data)
},
error: function (data) {
alert("请求出错")
}
}
)
})
</script>
</body>
</html>
// user-web/initialize/router.go
package initialize
import (
"github.com/gin-gonic/gin"
"mxshop-api/user-web/middlewares"
"mxshop-api/user-web/router"
)
func Routers() *gin.Engine {
r := gin.Default()
// 跨域
r.Use(middlewares.Cors())
apiGroup := r.Group("/v1")
// 分组注册接口
router.InitUserRouter(apiGroup)
return r
}
// user-web/middlewares/cors.go
package middlewares
import (
"github.com/gin-gonic/gin"
"net/http"
)
func Cors() gin.HandlerFunc {
return func(c *gin.Context) {
method := c.Request.Method
c.Header("Access-Control-Allow-Origin", "*")
c.Header("Access-Control-Allow-Headers",
"Content-Type, Token, x-token, AccessToken, X-CSRF-Token, Authorization ")
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PATCH, PUT")
// 可以有哪些headers
c.Header("Access-Control-Expose-Headers",
"Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Methods, "+
"Access-Control-Allow-Headers, Content-Type")
c.Header("Access-Control-Allow-Credentials", "true")
if method == "OPTIONS" {
c.AbortWithStatus(http.StatusNoContent)
}
}
}
2-9获取图片验证码
// user-web/api/user.go
// ...
// PasswordLogin 登录
func PasswordLogin(c *gin.Context) {
zap.S().Debug("[PasswordLogin] [用户登录]")
// 表单验证
passwordLoginForm := forms.PasswordLoginForm{}
if err := c.ShouldBind(&passwordLoginForm); err != nil {
// 如何返回错误信息
errs, ok := err.(validator.ValidationErrors)
if !ok {
c.JSON(http.StatusOK, gin.H{
"msg": err.Error(),
})
}
c.JSON(http.StatusBadRequest, gin.H{
"error": removeTopStruct(errs.Translate(global.Trans)),
})
return
}
// 验证码校验 true -> 执行该操作后清除(无论是否验证通过)
// if !store.Verify(passwordLoginForm.CaptchaId, passwordLoginForm.Captcha, true) {
// c.JSON(http.StatusBadRequest, gin.H{
// "captcha": "验证码错误",
// })
// fmt.Println(store.Get(passwordLoginForm.CaptchaId, false))
// return
// }
// 个人更喜欢这样
// 用户多次输入验证码, 如果不行就重新获取验证码再输入, 直到验证成功
// 验证码校验 true -> 执行该操作后清除(无论是否验证通过)
if !store.Verify(passwordLoginForm.CaptchaId, passwordLoginForm.Captcha, false) {
// ...
}
// 验证通过清除
store.Get(passwordLoginForm.CaptchaId, true)
// ...
}
// user-web/api/chaptcha.go
package api
import (
"github.com/gin-gonic/gin"
"github.com/mojocn/base64Captcha"
"go.uber.org/zap"
"net/http"
)
// 存在内存
var store = base64Captcha.DefaultMemStore
// GetCaptcha 获取验证码
func GetCaptcha(ctx *gin.Context) {
// 数字
d := base64Captcha.NewDriverDigit(80, 240, 5, 0.7, 80)
// 生成并缓存
cp := base64Captcha.NewCaptcha(d, store)
id, b64s, err := cp.Generate()
if err != nil {
zap.S().Errorf("生成验证码错误: %v", err.Error())
ctx.JSON(http.StatusInternalServerError, gin.H{
"msg": "生成验证码错误",
})
return
}
ctx.JSON(http.StatusOK, gin.H{
"captchaId": id,
"picPath": b64s,
})
}
// user-web/router/base.go
package router
import (
"github.com/gin-gonic/gin"
"mxshop-api/user-web/api"
)
// InitBaseRouter 通用功能的路由
func InitBaseRouter(r *gin.RouterGroup) {
baseRouter := r.Group("base")
{
baseRouter.GET("captcha", api.GetCaptcha)
}
}
// user-web/initialize/router.go
package initialize
import (
"github.com/gin-gonic/gin"
"mxshop-api/user-web/middlewares"
"mxshop-api/user-web/router"
)
func Routers() *gin.Engine {
r := gin.Default()
// 跨域
r.Use(middlewares.Cors())
apiGroup := r.Group("/v1")
// 分组注册接口
router.InitUserRouter(apiGroup)
router.InitBaseRouter(apiGroup)
return r
}
// user-web/forms/user.go
package forms
type PasswordLoginForm struct {
// 手机号码的格式如何校验? -> 自定义validate
Mobile string `form:"mobile" json:"mobile" binding:"required,mobile"`
Password string `form:"password" json:"password" binding:"required,min=3,max=20"`
// 验证码
Captcha string `form:"captcha" json:"captcha" binding:"required,min=5,max=5"`
// 验证码id
CaptchaId string `form:"captcha_id" json:"captcha_id" binding:"required"`
}
2-10阿里云发送短信
// user-web/router/base.go
package router
import (
"github.com/gin-gonic/gin"
"mxshop-api/user-web/api"
)
// InitBaseRouter 通用功能的路由
func InitBaseRouter(r *gin.RouterGroup) {
baseRouter := r.Group("base")
{
// 获取验证码图片
baseRouter.GET("captcha", api.GetCaptcha)
// 发送验证码
baseRouter.POST("send_sms", api.SendSms)
}
}
// user-web/api/sms.go
package api
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/jordan-wright/email"
"log"
"math/rand"
"mxshop-api/user-web/global"
"net/smtp"
"strings"
"time"
)
// GenerateSmsCode 生成指定长度的验证码
func GenerateSmsCode(width int) string {
numeric := [10]byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
r := len(numeric)
rand.Seed(time.Now().UnixNano())
var sb strings.Builder
for i := 0; i < width; i++ {
fmt.Fprintf(&sb, "%d", numeric[rand.Intn(r)])
}
return sb.String()
}
func SendSms(ctx *gin.Context) {
e := email.NewEmail()
//设置发送方的邮箱
e.From = fmt.Sprintf("malred <%d>", global.ServerConfig.EmailInfo.Sender)
// 设置接收方的邮箱
// 从前端拿到需要发送到的邮箱
e.To = []string{}
//设置主题
e.Subject = "验证码"
//设置文件发送的内容
e.Text = []byte(fmt.Sprintf("code: %s", GenerateSmsCode(6)))
//设置服务器相关的配置
err := e.Send("smtp.163.com:25", smtp.PlainAuth(
"", global.ServerConfig.EmailInfo.Sender, global.ServerConfig.EmailInfo.AuthCode, "smtp.163.com"))
if err != nil {
log.Fatal(err)
}
// 保存验证码到redis
}
// user-web/config/config.go
package config
// ...
type EmailConfig struct {
AuthCode string `mapstructure:"auth"`
Sender string `mapstructure:"sender"`
}
type ServerConfig struct {
Name string `mapstructure:"name"`
Port int `mapstructure:"port"`
UserSrvInfo UserSrvConfig `mapstructure:"user_srv"`
JWTInfo JWTConfig `mapstructure:"jwt"`
EmailInfo EmailConfig `mapstructure:"email"`
}
email:
auth: 你的校验码
sender: malguy2022@163.com
jwt:
key: 3hTQH29YPr5oCcwatf1a1KbcnAft8Hn1
name: user-web
port: 8021
user_srv:
host: 127.0.0.1
port: 50051
2-11redis保存验证码
// user-web/api/sms.go
package api
import (
"context"
"fmt"
"github.com/gin-gonic/gin"
redis "github.com/go-redis/redis/v8"
"github.com/jordan-wright/email"
"log"
"math/rand"
"mxshop-api/user-web/forms"
"mxshop-api/user-web/global"
"net/http"
"net/smtp"
"strings"
"time"
)
// GenerateSmsCode 生成指定长度的验证码
func GenerateSmsCode(width int) string {
numeric := [10]byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
r := len(numeric)
rand.Seed(time.Now().UnixNano())
var sb strings.Builder
for i := 0; i < width; i++ {
fmt.Fprintf(&sb, "%d", numeric[rand.Intn(r)])
}
return sb.String()
}
// SendSms 发送认证邮件
func SendSms(ctx *gin.Context) {
// 表单校验
form := forms.SendSmsForm{}
if err := ctx.ShouldBind(&form); err != nil {
HandleValidatorErr(ctx, err)
return
}
e := email.NewEmail()
//设置发送方的邮箱
e.From = fmt.Sprintf("malred <%s>", global.ServerConfig.EmailInfo.Sender)
// 设置接收方的邮箱
// 从前端拿到需要发送到的邮箱
//user_email := "13695024677@163.com"
e.To = []string{form.Email}
//设置主题
e.Subject = "mxshop 验证码"
//设置文件发送的内容
code := GenerateSmsCode(6)
e.Text = []byte(fmt.Sprintf("code: %s", code))
//设置服务器相关的配置
err := e.Send("smtp.163.com:25", smtp.PlainAuth(
"", global.ServerConfig.EmailInfo.Sender,
global.ServerConfig.EmailInfo.AuthCode, "smtp.163.com"))
if err != nil {
log.Fatal(err)
}
// 保存验证码到redis
rdb := redis.NewClient(&redis.Options{
Addr: fmt.Sprintf("%s:%d",
global.ServerConfig.RedisInfo.Host,
global.ServerConfig.RedisInfo.Port),
})
rdb.Set(context.Background(), form.Email, code,
time.Duration(global.ServerConfig.EmailInfo.Expire)*time.Second)
ctx.JSON(http.StatusOK, gin.H{
"msg": "发送成功",
})
}
// user-web/config/config.go
package config
// ...
type EmailConfig struct {
AuthCode string `mapstructure:"auth"`
Sender string `mapstructure:"sender"`
Expire int `mapstructure:"expire"`
}
type ServerConfig struct {
Name string `mapstructure:"name"`
Port int `mapstructure:"port"`
UserSrvInfo UserSrvConfig `mapstructure:"user_srv"`
JWTInfo JWTConfig `mapstructure:"jwt"`
EmailInfo EmailConfig `mapstructure:"email"`
RedisInfo RedisConfig `mapstructure:"redis"`
}
// user-web/api/user.go
// ...
func HandleValidatorErr(c *gin.Context, err error) {
// 如何返回错误信息
errs, ok := err.(validator.ValidationErrors)
if !ok {
c.JSON(http.StatusOK, gin.H{
"msg": err.Error(),
})
}
c.JSON(http.StatusBadRequest, gin.H{
"error": removeTopStruct(errs.Translate(global.Trans)),
})
}
// user-web/forms/sms.go
package forms
type SendSmsForm struct {
Email string `form:"email" json:"email" binding:"required,email"`
// 有可能有很多种验证码, 注册|登录|...
Type string `form:"type" json:"type" binding:"required,oneof=register login"`
}
email:
auth:
expire: 300
sender: malguy2022@163.com
# ...
2-12用户注册接口
// user-web/api/user.go
// ...
// Register 用户注册
func Register(c *gin.Context) {
// 表单验证
form := forms.RegisterForm{}
if err := c.ShouldBind(&form); err != nil {
HandleValidatorErr(c, err)
return
}
rdb := redis.NewClient(&redis.Options{
Addr: fmt.Sprintf("%s:%d",
global.ServerConfig.RedisInfo.Host,
global.ServerConfig.RedisInfo.Port),
})
val, err := rdb.Get(context.Background(), form.Email).Result()
if err == redis.Nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": "验证码不存在,请重新获取",
})
return
}
if val != form.Code {
c.JSON(http.StatusBadRequest, gin.H{
"code": "验证码错误",
})
return
}
info := global.ServerConfig.UserSrvInfo
userConn, err := grpc.Dial(fmt.Sprintf("%s:%d", info.Host, info.Port), grpc.WithInsecure())
//userConn, err := grpc.Dial(fmt.Sprintf("%s:%d", info.Host, info.Port),
// grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
zap.S().Errorw("[Register] 连接 [用户服务失败] ", "msg", err.Error())
return
}
cli := proto.NewUserClient(userConn)
createInfo := &proto.CreateUserInfo{
NickName: form.Mobile,
Password: form.Password,
Mobile: form.Mobile,
}
user, err := cli.CreateUser(context.Background(), createInfo)
if err != nil {
zap.S().Errorf("[Register] 注册 [新建用户失败]: %s", err.Error())
HandleGrpcErrorToHttp(err, c)
return
}
// 注册完成就登录
// 生成 jwt token
j := middlewares.NewJWT()
claims := models.CustomClaims{
ID: uint(user.Id),
NickName: user.NickName,
AuthorityId: uint(user.Role),
StandardClaims: jwt.StandardClaims{
Audience: "",
// 30天过期
ExpiresAt: time.Now().Unix() + 60*60*24*30,
// 哪个机构认证的
Issuer: "malred",
// 签名生效时间
NotBefore: time.Now().Unix(),
},
}
token, err := j.CreateToken(claims)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"msg": "生成token失败",
})
}
c.JSON(http.StatusOK, gin.H{
"id": user.Id,
"nick_name": user.NickName,
"token": token,
"expired_at": (time.Now().Unix() + 60*60*24*30) * 1000,
"msg": "登录成功",
})
}
// user-web/router/user.go
package router
import (
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"mxshop-api/user-web/api"
"mxshop-api/user-web/middlewares"
)
func InitUserRouter(group *gin.RouterGroup) {
userRouter := group.Group("user")
zap.S().Info("配置用户相关的url")
{
userRouter.GET("list", middlewares.JWTAuth(), middlewares.IsAdminAuth(), api.GetUserList)
userRouter.POST("pwd_login", api.PasswordLogin)
userRouter.POST("register", api.Register)
}
}
// user-web/forms/user.go
package forms
// ...
// RegisterForm 注册表单
type RegisterForm struct {
Email string `form:"email" json:"email" binding:"required,email"`
Mobile string `form:"mobile" json:"mobile" binding:"required,mobile"`
Password string `form:"password" json:"password" binding:"required,min=3,max=20"`
// 验证码
Code string `form:"code" json:"code" binding:"required,min=6,max=6"`
}
第10周 服务注册发现、配置中心、负载均衡
第1章注册中心-consul
1-1什么是服务注册和发现以及技术选型
1-2consul的安装和配置
1-3服务注册和注销
1-4go集成consul
// user-web/router/base.go
package router
import (
"github.com/gin-gonic/gin"
"mxshop-api/user-web/api"
"net/http"
)
// InitBaseRouter 通用功能的路由
func InitBaseRouter(r *gin.RouterGroup) {
baseRouter := r.Group("base")
{
// 获取验证码图片
baseRouter.GET("captcha", api.GetCaptcha)
// 发送验证码
baseRouter.POST("send_sms", api.SendSms)
// 健康检查
baseRouter.GET("health", func(c *gin.Context) {
c.JSON(http.StatusOK, nil)
})
}
}
// user_srv/main.go
package main
import (
"flag"
"fmt"
"google.golang.org/grpc"
"mxshop_srvs/user_srv/handler"
"mxshop_srvs/user_srv/proto"
"net"
)
func main() {
// 让用户从命令行传递
IP := flag.String("ip", "0.0.0.0", "ip地址")
Port := flag.Int("port", 50051, "端口号")
flag.Parse()
fmt.Println("ip: ", *IP)
fmt.Println("port: ", *Port)
server := grpc.NewServer()
proto.RegisterUserServer(server, &handler.UserServer{})
lis, err := net.Listen("tcp", fmt.Sprintf("%s:%d", *IP, *Port))
if err != nil {
panic("failed to listen: " + err.Error())
}
err = server.Serve(lis)
if err != nil {
panic("failed to start grpc: " + err.Error())
}
}
1-5为grpc服务添加viper和zap
// user_srv/global/global.go
package global
import (
"gorm.io/gorm"
"mxshop_srvs/user_srv/config"
)
var (
DB *gorm.DB
ServerConfig config.ServerConfig
)
# user_srv/config-debug.yaml
mysql:
host: 127.0.0.1
port: 3307
user: root
password: 123456
db: mxshop_user_srv
// user_srv/config/config.go
package config
type MysqlConfig struct {
Host string `mapstructure:"host" json:"host"`
Port int `mapstructure:"port" json:"port"`
Name string `mapstructure:"db" json:"db"`
User string `mapstructure:"user" json:"user"`
Password string `mapstructure:"password" json:"password"`
}
type ServerConfig struct {
MysqlInfo MysqlConfig `mapstructure:"mysql" json:"mysql"`
}
// user_srv/initialize/logger.go
package initialize
import "go.uber.org/zap"
func InitLogger() {
// 日志分级别(debug,info,warn,err,fetal), production打印info及以上级别
//logger, _ := zap.NewProduction()
logger, _ := zap.NewDevelopment() // dev日志不是json格式
// zap.S() 里的logger对象是没有配置打印的, 我们要自己设置全局logger
zap.ReplaceGlobals(logger)
}
// user_srv/initialize/db.go
package initialize
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"gorm.io/gorm/schema"
"log"
"mxshop_srvs/user_srv/global"
"mxshop_srvs/user_srv/model"
"os"
"time"
)
func InitDB() {
m := global.ServerConfig.MysqlInfo
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
m.User, m.Password, m.Host, m.Port, m.Name)
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
logger.Config{
SlowThreshold: time.Second, // 慢 SQL 阈值
LogLevel: logger.Info,
Colorful: true, // 禁用彩色打印
},
)
// 全局模式
var err error
global.DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
NamingStrategy: schema.NamingStrategy{
// 为false会根据 结构体名+s 作为表名
SingularTable: true,
},
Logger: newLogger,
})
if err != nil {
panic(err)
}
// 定义一个表结构, 生成对应表
_ = global.DB.AutoMigrate(&model.User{})
}
// user_srv/initialize/config.go
package initialize
import (
"fmt"
"github.com/spf13/viper"
"mxshop_srvs/user_srv/global"
)
func GetEnvInfo(env string) bool {
viper.AutomaticEnv()
// 设置的环境变量要生效, 要重启ide(不要用缓存清除重启)
return viper.GetBool(env)
}
// InitConfig 从配置文件读取配置
func InitConfig() {
debug := GetEnvInfo("MXSHOP_DEBUG")
configFilePrefix := "config"
configFileName := fmt.Sprintf("user_srv/%s-product.yaml", configFilePrefix)
// 线上线下配置隔离
if debug {
configFileName = fmt.Sprintf("user_srv/%s-debug.yaml", configFilePrefix)
}
v := viper.New()
v.SetConfigFile(configFileName)
if err := v.ReadInConfig(); err != nil {
panic(err)
}
if err := v.Unmarshal(&global.ServerConfig); err != nil {
panic(err)
}
}
// user_srv/main.go
package main
import (
"flag"
"fmt"
"go.uber.org/zap"
"google.golang.org/grpc"
"mxshop_srvs/user_srv/handler"
"mxshop_srvs/user_srv/initialize"
"mxshop_srvs/user_srv/proto"
"net"
)
func main() {
// 让用户从命令行传递
IP := flag.String("ip", "0.0.0.0", "ip地址")
Port := flag.Int("port", 50051, "端口号")
// 初始化日志
initialize.InitLogger()
initialize.InitConfig()
initialize.InitDB()
flag.Parse()
zap.S().Info("ip: ", *IP)
zap.S().Info("port: ", *Port)
// ...
}
1-6grpc服务如何进行健康检查
// user_srv/main.go
package main
import (
"flag"
"fmt"
"go.uber.org/zap"
"google.golang.org/grpc"
"google.golang.org/grpc/health"
"google.golang.org/grpc/health/grpc_health_v1"
"mxshop_srvs/user_srv/handler"
"mxshop_srvs/user_srv/initialize"
"mxshop_srvs/user_srv/proto"
"net"
)
func main() {
// ...
lis, err := net.Listen("tcp", fmt.Sprintf("%s:%d", *IP, *Port))
if err != nil {
panic("failed to listen: " + err.Error())
}
// 注册服务健康检查
grpc_health_v1.RegisterHealthServer(server, health.NewServer())
err = server.Serve(lis)
if err != nil {
panic("failed to start grpc: " + err.Error())
}
}
1-7将grpc服务注册到consul中
// user_srv/main.go
package main
import (
"flag"
"fmt"
"github.com/hashicorp/consul/api"
"go.uber.org/zap"
"google.golang.org/grpc"
"google.golang.org/grpc/health"
"google.golang.org/grpc/health/grpc_health_v1"
"mxshop_srvs/user_srv/global"
"mxshop_srvs/user_srv/handler"
"mxshop_srvs/user_srv/initialize"
"mxshop_srvs/user_srv/proto"
"net"
)
func main() {
// ...
// 注册服务健康检查
grpc_health_v1.RegisterHealthServer(server, health.NewServer())
// 服务注册
cfg := api.DefaultConfig()
sc := global.ServerConfig
cfg.Address = fmt.Sprintf("%s:%d", sc.ConsulInfo.Host, sc.ConsulInfo.Port)
cli, err := api.NewClient(cfg)
if err != nil {
panic(err)
}
// 生成对应的检查对象
check := &api.AgentServiceCheck{
// 0.0.0.0可以用于监听,但是不能用来访问(比如consul进行健康检测)
GRPC: fmt.Sprintf("127.0.0.1:50051"),
Timeout: "5s",
Interval: "5s",
DeregisterCriticalServiceAfter: "15s",
}
// 创建注册对象
registration := new(api.AgentServiceRegistration)
registration.Name = sc.Name
registration.ID = sc.Name
registration.Port = *Port
registration.Tags = []string{"imooc", "user_srv"}
registration.Address = "127.0.0.1"
registration.Check = check
err = cli.Agent().ServiceRegister(registration)
if err != nil {
return
}
err = server.Serve(lis)
if err != nil {
panic("failed to start grpc: " + err.Error())
}
}
// user_srv/config/config.go
package config
type MysqlConfig struct {
// ...
}
type ConsulConfig struct {
Host string `mapstructure:"host" json:"host"`
Port int `mapstructure:"port" json:"port"`
}
type ServerConfig struct {
Host string `mapstructure:"host" json:"host"`
Port int `mapstructure:"port" json:"port"`
// 用于服务注册
Name string `mapstructure:"name" json:"name"`
MysqlInfo MysqlConfig `mapstructure:"mysql" json:"mysql"`
ConsulInfo MysqlConfig `mapstructure:"consul" json:"consul"`
}
# user_srv/config-debug.yaml
mysql:
host: 127.0.0.1
port: 3307
user: root
password: 123456
db: mxshop_user_srv
consul:
host: 127.0.0.1
port: 8500
name: user_srv
host: 0.0.0.0
port: 50051
1-8gin集成consul
// user-web/api/user.go
// ...
// GetUserList 获取用户列表 - 分页
func GetUserList(ctx *gin.Context) {
zap.S().Debug("[GetUserList] 获取 [用户列表]")
// 从注册中心获取用户服务消息
cfg := api.DefaultConfig()
g := global.ServerConfig
cfg.Address = fmt.Sprintf("%s:%d", g.ConsulInfo.Host, g.ConsulInfo.Port)
userSrvHost := ""
userSrvPort := 0
cli, err := api.NewClient(cfg)
if err != nil {
panic(err)
}
data, err := cli.Agent().ServicesWithFilter(fmt.Sprintf(`Service == "%s"`, g.UserSrvInfo.Name))
if err != nil {
panic(err)
}
for _, v := range data {
userSrvHost = v.Address
userSrvPort = v.Port
break
}
if userSrvHost == "" {
ctx.JSON(http.StatusBadRequest, gin.H{
"msg": "用户服务不可达",
})
return
}
//info := global.ServerConfig.UserSrvInfo
// 连接user grpc服务
userConn, err := grpc.Dial(fmt.Sprintf("%s:%d", userSrvHost, userSrvPort), grpc.WithInsecure())
if err != nil {
zap.S().Errorw("[GetUserList] 连接 [用户服务失败] ", "msg", err.Error())
return
}
claims, _ := ctx.Get("claims")
currentUser := claims.(*models.CustomClaims)
zap.S().Infof("访问用户: %d", currentUser.ID)
// 创建client
client := proto.NewUserClient(userConn)
// 接收请求参数
pn := ctx.DefaultQuery("pn", "0")
pnInt, _ := strconv.Atoi(pn)
pSize := ctx.DefaultQuery("psize", "10")
pSizeInt, _ := strconv.Atoi(pSize)
// 调用接口
rsp, err := client.GetUserList(context.Background(), &proto.PageInfo{
Pn: uint32(pnInt),
PSize: uint32(pSizeInt),
})
if err != nil {
zap.S().Errorw("[GetUserList] 查询 [用户列表] 失败", "msg", err.Error())
HandleGrpcErrorToHttp(err, ctx)
return
}
res := make([]interface{}, 0)
for _, value := range rsp.Data {
//data := make(map[string]interface{})
//data["id"] = value.Id
//data["name"] = value.NickName
//data["birthday"] = value.BirthDay
//data["gender"] = value.Gender
//data["mobile"] = value.Mobile
//res = append(res, data)
user := response.UserResponse{
Id: value.Id,
NickName: value.NickName,
//Birthday: time.Unix(int64(value.BirthDay), 0) ,
//Birthday: time.Unix(int64(value.BirthDay), 0).Format("2006-01-02"),
Birthday: response.JsonTime(time.Unix(int64(value.BirthDay), 0)),
Gender: value.Gender,
Mobile: value.Mobile,
}
res = append(res, user)
}
ctx.JSON(http.StatusOK, res)
}
// user-web/config/config.go
package config
type UserSrvConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
Name string `mapstructure:"name"`
}
// ...
type ConsulConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
}
type ServerConfig struct {
Name string `mapstructure:"name"`
Port int `mapstructure:"port"`
// user-grpc 服务消息
UserSrvInfo UserSrvConfig `mapstructure:"user_srv"`
JWTInfo JWTConfig `mapstructure:"jwt"`
EmailInfo EmailConfig `mapstructure:"email"`
RedisInfo RedisConfig `mapstructure:"redis"`
ConsulInfo ConsulConfig `mapstructure:"consul"`
}
# ...
consul:
host: 127.0.0.1
port: 8500
name: user-web
port: 8021
user_srv:
host: 127.0.0.1
name: user_srv
port: 50051
1-9将用户的grpc连接配置到全局共用
// user-web/api/user.go
package api
import (
"context"
"fmt"
"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"github.com/go-redis/redis/v8"
"go.uber.org/zap"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"mxshop-api/user-web/forms"
"mxshop-api/user-web/global"
"mxshop-api/user-web/global/response"
"mxshop-api/user-web/middlewares"
"mxshop-api/user-web/models"
"mxshop-api/user-web/proto"
"net/http"
"strconv"
"strings"
"time"
)
// ...
// GetUserList 获取用户列表 - 分页
func GetUserList(ctx *gin.Context) {
zap.S().Debug("[GetUserList] 获取 [用户列表]")
// ...
// 调用接口
rsp, err := global.UserSrvClient.GetUserList(context.Background(), &proto.PageInfo{
Pn: uint32(pnInt),
PSize: uint32(pSizeInt),
})
if err != nil {
zap.S().Errorw("[GetUserList] 查询 [用户列表] 失败 ", "msg", err.Error())
HandleGrpcErrorToHttp(err, ctx)
return
}
// ...
}
// PasswordLogin 登录
func PasswordLogin(c *gin.Context) {
zap.S().Debug("[PasswordLogin] [用户登录]")
// ...
// 登录
if user, err := global.UserSrvClient.GetUserMobile(context.Background(), &proto.MobileRequest{
Mobile: passwordLoginForm.Mobile,
}); err != nil {
if e, ok := status.FromError(err); ok {
// ...
}
} else {
// 查询到了用户, 没有检查密码
if rsp, err := global.UserSrvClient.CheckPassword(context.Background(), &proto.PasswordCheckInfo{
Password: passwordLoginForm.Password,
EncryptedPassword: user.Password,
}); err != nil {
c.JSON(http.StatusInternalServerError, map[string]string{
"password": "登录失败",
})
} else {
// ...
}
}
}
// Register 用户注册
func Register(c *gin.Context) {
// ...
user, err := global.UserSrvClient.CreateUser(context.Background(), createInfo)
// ...
}
// user-web/main.go
package main
import (
"fmt"
"github.com/gin-gonic/gin/binding"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
"go.uber.org/zap"
"mxshop-api/user-web/global"
"mxshop-api/user-web/initialize"
myvalidator "mxshop-api/user-web/validator"
)
func main() {
// 1. 初始化logger
initialize.InitLogger()
// 初始化配置
initialize.InitConfig()
// 2. 初始化router
r := initialize.Routers()
// 初始化翻译
if err := initialize.InitTrans("zh"); err != nil {
panic(err)
}
// ...
// 初始化user_src连接(consul grpc)
initialize.InitSrvConn()
// ...
}
// user-web/initialize/srv_conn.go
package initialize
import (
"fmt"
"github.com/hashicorp/consul/api"
"go.uber.org/zap"
"google.golang.org/grpc"
"mxshop-api/user-web/global"
"mxshop-api/user-web/proto"
)
func InitSrvConn() {
// 从注册中心获取用户服务消息
cfg := api.DefaultConfig()
g := global.ServerConfig
cfg.Address = fmt.Sprintf("%s:%d", g.ConsulInfo.Host, g.ConsulInfo.Port)
userSrvHost := ""
userSrvPort := 0
cli, err := api.NewClient(cfg)
if err != nil {
panic(err)
}
data, err := cli.Agent().ServicesWithFilter(fmt.Sprintf(`Service == "%s"`, g.UserSrvInfo.Name))
if err != nil {
panic(err)
}
for _, v := range data {
userSrvHost = v.Address
userSrvPort = v.Port
break
}
if userSrvHost == "" {
zap.S().Fatal("[InitSrvConn] 连接 [用户服务失败] ", "msg", err.Error())
return
}
//info := global.ServerConfig.UserSrvInfo
// 连接user grpc服务
//userConn, err := grpc.Dial(fmt.Sprintf("%s:%d", userSrvHost, userSrvPort), grpc.WithInsecure())
userConn, err := grpc.Dial(
fmt.Sprintf("%s:%d", userSrvHost, userSrvPort),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
zap.S().Errorw("[GetUserList] 连接 [用户服务失败] ", "msg", err.Error())
return
}
// 后续用户服务下线|改端口或ip 怎么办? -> 负载均衡来处理
userClient := proto.NewUserClient(userConn)
global.UserSrvClient = userClient
}
// user-web/global/global.go
package global
import (
ut "github.com/go-playground/universal-translator"
"mxshop-api/user-web/config"
"mxshop-api/user-web/proto"
)
var (
ServerConfig *config.ServerConfig = &config.ServerConfig{}
Trans ut.Translator
UserSrvClient proto.UserClient
)
第2章负载均衡
2-1动态获取可用端口
// user-web/main.go
package main
import (
"fmt"
"github.com/gin-gonic/gin/binding"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
"github.com/spf13/viper"
"go.uber.org/zap"
"mxshop-api/user-web/global"
"mxshop-api/user-web/initialize"
"mxshop-api/user-web/utils"
myvalidator "mxshop-api/user-web/validator"
)
func main() {
// ...
// 初始化user_src连接(consul grpc)
initialize.InitSrvConn()
//port:=global.ServerConfig.Port
viper.AutomaticEnv()
// 本地开发环境端口固定, 线上环境自动获取端口号
debug := viper.GetBool("MXSHOP_DEBUG")
if !debug {
port, err := utils.GetFreePort()
if err == nil {
global.ServerConfig.Port = port
}
}
// ...
}
// user-web/utils/addr.go
package utils
import (
"net"
)
func GetFreePort() (int, error) {
addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
if err != nil {
return 0, err
}
l, err := net.ListenTCP("tcp", addr)
if err != nil {
return 0, err
}
defer l.Close()
return l.Addr().(*net.TCPAddr).Port, nil
}
// user_srv/main.go
package main
import (
"flag"
"fmt"
"github.com/hashicorp/consul/api"
"go.uber.org/zap"
"google.golang.org/grpc"
"google.golang.org/grpc/health"
"google.golang.org/grpc/health/grpc_health_v1"
"mxshop_srvs/user_srv/global"
"mxshop_srvs/user_srv/handler"
"mxshop_srvs/user_srv/initialize"
"mxshop_srvs/user_srv/proto"
"mxshop_srvs/user_srv/utils"
"net"
)
func main() {
// 让用户从命令行传递
//IP := flag.String("ip", "0.0.0.0", "ip地址")
IP := flag.String("ip", "0.0.0.0", "ip地址")
Port := flag.Int("port", 0, "端口号")
// 初始化日志
initialize.InitLogger()
initialize.InitConfig()
initialize.InitDB()
zap.S().Info(global.ServerConfig)
flag.Parse()
zap.S().Info("ip: ", *IP)
if *Port == 0 {
*Port, _ = utils.GetFreePort()
}
zap.S().Info("port: ", *Port)
// ...
// 生成对应的检查对象
check := &api.AgentServiceCheck{
// 0.0.0.0可以用于监听,但是不能用来访问(比如consul进行健康检测)
GRPC: fmt.Sprintf("127.0.0.1:%d", *Port),
Timeout: "5s",
Interval: "5s",
DeregisterCriticalServiceAfter: "15s",
}
// ...
}
// user_srv/utils/addr.go
package utils
import (
"net"
)
func GetFreePort() (int, error) {
addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
if err != nil {
return 0, err
}
l, err := net.ListenTCP("tcp", addr)
if err != nil {
return 0, err
}
defer l.Close()
return l.Addr().(*net.TCPAddr).Port, nil
}
2-2什么是负载均衡,负载均衡的策略有哪些?
2-3常用负载均衡算法
2-4gin从consul中同步服务信息并进行负载均衡
// user_srv/main.go
package main
import (
"flag"
"fmt"
"github.com/hashicorp/consul/api"
"github.com/satori/go.uuid"
"go.uber.org/zap"
"google.golang.org/grpc"
"google.golang.org/grpc/health"
"google.golang.org/grpc/health/grpc_health_v1"
"mxshop_srvs/user_srv/global"
"mxshop_srvs/user_srv/handler"
"mxshop_srvs/user_srv/initialize"
"mxshop_srvs/user_srv/proto"
"mxshop_srvs/user_srv/utils"
"net"
"os"
"os/signal"
"syscall"
)
func main() {
// ...
// 创建注册对象
registration := new(api.AgentServiceRegistration)
registration.Name = sc.Name
// 名称相同id不同, consul就不会覆盖
serviceID := fmt.Sprintf("%s", uuid.NewV4())
//registration.ID = sc.Name
registration.ID = serviceID
// ...
go func() {
// 这里是阻塞代码, 后面的执行不到, 所以改造为异步
err = server.Serve(lis)
if err != nil {
panic("failed to start grpc: " + err.Error())
}
}()
// 接收终止信号, 注销服务
quit := make(chan os.Signal)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
if err = cli.Agent().ServiceDeregister(serviceID); err != nil {
zap.S().Info("注销失败")
panic(err)
}
zap.S().Info("注销成功")
}
// user-web/test/grpclb_test/main.go
package main
import (
"context"
"fmt"
_ "github.com/mbobakov/grpc-consul-resolver"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"log"
"mxshop-api/user-web/proto"
)
func main() {
conn, err := grpc.Dial(
"consul://127.0.0.1:8500/user_srv?wait=14s&tag=imooc",
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy": "round_robin"}`),
)
if err != nil {
log.Fatal(err)
}
defer conn.Close()
usc := proto.NewUserClient(conn)
for i := 0; i < 10; i++ {
rsp, err := usc.GetUserList(context.Background(), &proto.PageInfo{
Pn: 1,
PSize: 2,
})
if err != nil {
panic(err)
}
for i, data := range rsp.Data {
fmt.Println(i, data)
}
}
}
2-6gin集成grpc的负载均衡
// user-web/initialize/srv_conn.go
package initialize
import (
"fmt"
"github.com/hashicorp/consul/api"
_ "github.com/mbobakov/grpc-consul-resolver"
"go.uber.org/zap"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"mxshop-api/user-web/global"
"mxshop-api/user-web/proto"
)
func InitSrvConn() {
g := global.ServerConfig
conn, err := grpc.Dial(
fmt.Sprintf("consul://%s:%d/%s?wait=14s",
g.ConsulInfo.Host, g.ConsulInfo.Port, g.UserSrvInfo.Name,
),
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy": "round_robin"}`),
)
if err != nil {
zap.S().Fatalf("[InitSrvConn] 连接 [用户服务失败]")
}
global.UserSrvClient = proto.NewUserClient(conn)
}
第3章分布式配置中心
3-1为什么需要配置中心
3-2配置中心选型-apollovsnacos
3-3nacos的安装
3-4nacos的组、配置集、命名空间
3-5通过api获取nacos的配置以及nacos的配置更新
// user-web/test/nacos_test/main.go
package main
import (
"fmt"
"github.com/nacos-group/nacos-sdk-go/v2/clients"
"github.com/nacos-group/nacos-sdk-go/v2/common/constant"
"github.com/nacos-group/nacos-sdk-go/v2/vo"
"time"
)
func main() {
sc := []constant.ServerConfig{
{
IpAddr: "127.0.0.1",
ContextPath: "/nacos",
Port: 8848,
Scheme: "http",
},
}
cc := constant.ClientConfig{
NamespaceId: "acb01f00-6f8f-40d6-b9c7-57732c5b5ebb",
TimeoutMs: 5000,
NotLoadCacheAtStart: true,
LogDir: "tmp/nacos/log",
CacheDir: "tmp/nacos/cache",
LogLevel: "debug",
}
// 创建动态配置客户端的另一种方式 (推荐)
configClient, err := clients.NewConfigClient(
vo.NacosClientParam{
ClientConfig: &cc,
ServerConfigs: sc,
},
)
if err != nil {
panic(err)
}
// 获取配置
content, err := configClient.GetConfig(
vo.ConfigParam{
DataId: "user-web.yaml",
Group: "dev",
},
)
if err != nil {
panic(err)
}
fmt.Println(content)
// 监听配置变化
err = configClient.ListenConfig(vo.ConfigParam{
DataId: "user-web.yaml",
Group: "dev",
OnChange: func(namespace, group, dataId, data string) {
fmt.Println("配置文件变化")
fmt.Println("group:" + group + ", dataId:" + dataId + ", data:" + data)
},
})
if err != nil {
panic(err)
}
time.Sleep(300 * time.Second)
}
3-6gin集成nacos
// user-web/test/nacos_test/main.go
package main
import (
"encoding/json"
"fmt"
"github.com/nacos-group/nacos-sdk-go/v2/clients"
"github.com/nacos-group/nacos-sdk-go/v2/common/constant"
"github.com/nacos-group/nacos-sdk-go/v2/vo"
"time"
)
type UserSrvConfig struct {
Host string `json:"host"`
Port int `json:"port"`
Name string `json:"name"`
}
type JWTConfig struct {
SigningKey string `json:"key"`
}
type EmailConfig struct {
AuthCode string `json:"auth"`
Sender string `json:"sender"`
Expire int `json:"expire"`
}
type RedisConfig struct {
Host string `json:"host"`
Port int `json:"port"`
}
type ConsulConfig struct {
Host string `json:"host"`
Port int `json:"port"`
}
type ServerConfig struct {
ConsulInfo ConsulConfig `json:"consul"`
EmailInfo EmailConfig `json:"email"`
Name string `json:"name"`
Port int `json:"port"`
// user-grpc 服务消息
UserSrvInfo UserSrvConfig `json:"user_srv"`
JWTInfo JWTConfig `json:"jwt"`
RedisInfo RedisConfig `json:"redis"`
}
func main() {
sc := []constant.ServerConfig{
{
IpAddr: "127.0.0.1",
ContextPath: "/nacos",
Port: 8848,
Scheme: "http",
},
}
cc := constant.ClientConfig{
NamespaceId: "acb01f00-6f8f-40d6-b9c7-57732c5b5ebb",
TimeoutMs: 5000,
NotLoadCacheAtStart: true,
LogDir: "tmp/nacos/log",
// 网络中获取不到, 会从缓存拿
CacheDir: "tmp/nacos/cache",
LogLevel: "debug",
}
// 创建动态配置客户端的另一种方式 (推荐)
configClient, err := clients.NewConfigClient(
vo.NacosClientParam{
ClientConfig: &cc,
ServerConfigs: sc,
},
)
if err != nil {
panic(err)
}
// 获取配置
content, err := configClient.GetConfig(
vo.ConfigParam{
DataId: "user-web.json",
Group: "dev",
},
)
if err != nil {
panic(err)
}
fmt.Println(content)
serverConfig := ServerConfig{}
// 转换为json需要tag
err = json.Unmarshal([]byte(content), &serverConfig)
if err != nil {
panic(err)
return
}
fmt.Println(serverConfig)
// 监听配置变化
err = configClient.ListenConfig(vo.ConfigParam{
DataId: "user-web.json",
Group: "dev",
OnChange: func(namespace, group, dataId, data string) {
fmt.Println("配置文件变化")
fmt.Println("group:" + group + ", dataId:" + dataId + ", data:" + data)
},
})
if err != nil {
panic(err)
}
time.Sleep(300 * time.Second)
}
// user-web/initialize/config.go
// ...
// InitConfig 从nacos中获取配置
func InitConfig() {
debug := GetEnvInfo("MXSHOP_DEBUG")
configFilePrefix := "config"
configFileName := fmt.Sprintf("user-web/%s-product.yaml", configFilePrefix)
// 线上线下配置隔离
if debug {
configFileName = fmt.Sprintf("user-web/%s-debug.yaml", configFilePrefix)
}
v := viper.New()
v.SetConfigFile(configFileName)
err := v.ReadInConfig()
//var sc config.ServerConfig
err = v.Unmarshal(&global.NacosConfig)
zap.S().Infof("配置消息: %v", global.NacosConfig)
if err != nil {
zap.S().Fatalf("viper转换配置失败: %s", err.Error())
return
}
// 从nacos获取配置
sc := []constant.ServerConfig{
{
IpAddr: global.NacosConfig.Host,
ContextPath: "/nacos",
Port: global.NacosConfig.Port,
Scheme: "http",
},
}
cc := constant.ClientConfig{
NamespaceId: global.NacosConfig.Namespace,
TimeoutMs: 5000,
NotLoadCacheAtStart: true,
LogDir: "tmp/nacos/log",
// 网络中获取不到, 会从缓存拿
CacheDir: "tmp/nacos/cache",
LogLevel: "debug",
}
// 创建动态配置客户端的另一种方式 (推荐)
configClient, err := clients.NewConfigClient(
vo.NacosClientParam{
ClientConfig: &cc,
ServerConfigs: sc,
},
)
if err != nil {
zap.S().Fatalf("nacos创建客户端失败: %s", err.Error())
return
}
// 获取配置
content, err := configClient.GetConfig(
vo.ConfigParam{
DataId: global.NacosConfig.Dataid,
Group: global.NacosConfig.Group,
},
)
if err != nil {
zap.S().Fatalf("读取nacos配置失败: %s", err.Error())
return
}
// 转换为json需要tag
err = json.Unmarshal([]byte(content), &global.ServerConfig)
if err != nil {
zap.S().Fatalf("nacos配置转换struct失败: %s", err.Error())
return
}
// 监听配置变化 (好像不是很必要)
err = configClient.ListenConfig(vo.ConfigParam{
DataId: global.NacosConfig.Dataid,
Group: global.NacosConfig.Group,
OnChange: func(namespace, group, dataId, data string) {
//zap.S().Infof("配置文件变化: \n%s\n", data)
zap.S().Info("配置文件变化")
// 转换为json需要tag
err = json.Unmarshal([]byte(data), &global.ServerConfig)
if err != nil {
zap.S().Fatalf("nacos配置转换struct失败: %s", err.Error())
return
}
zap.S().Debugf("当前配置: \n%v\n", global.ServerConfig)
},
})
if err != nil {
panic(err)
}
}
// user-web/config/config.go
package config
type UserSrvConfig struct {
Host string `mapstructure:"host" json:"host"`
Port int `mapstructure:"port" json:"port"`
Name string `mapstructure:"name" json:"name"`
}
type JWTConfig struct {
SigningKey string `mapstructure:"key" json:"key"`
}
type EmailConfig struct {
AuthCode string `mapstructure:"auth" json:"auth"`
Sender string `mapstructure:"sender" json:"sender"`
Expire int `mapstructure:"expire" json:"expire"`
}
type RedisConfig struct {
Host string `mapstructure:"host" json:"host"`
Port int `mapstructure:"port" json:"port"`
}
type ConsulConfig struct {
Host string `mapstructure:"host" json:"host"`
Port int `mapstructure:"port" json:"port"`
}
type ServerConfig struct {
Name string `mapstructure:"name" json:"name"`
Port int `mapstructure:"port" json:"port"`
// user-grpc 服务消息
UserSrvInfo UserSrvConfig `mapstructure:"user_srv" json:"user_srv"`
JWTInfo JWTConfig `mapstructure:"jwt" json:"jwt"`
EmailInfo EmailConfig `mapstructure:"email" json:"email"`
RedisInfo RedisConfig `mapstructure:"redis" json:"redis"`
ConsulInfo ConsulConfig `mapstructure:"consul" json:"consul"`
}
type NacosConfig struct {
Host string `mapstructure:"host"`
Port uint64 `mapstructure:"port"`
Namespace string `mapstructure:"namespace"`
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
Dataid string `mapstructure:"dataid"`
Group string `mapstructure:"group"`
}
// user-web/global/global.go
package global
import (
ut "github.com/go-playground/universal-translator"
"mxshop-api/user-web/config"
"mxshop-api/user-web/proto"
)
var (
ServerConfig *config.ServerConfig = &config.ServerConfig{}
Trans ut.Translator
UserSrvClient proto.UserClient
NacosConfig *config.NacosConfig = &config.NacosConfig{}
)
host: 127.0.0.1
port: 8848
namespace: acb01f00-6f8f-40d6-b9c7-57732c5b5ebb
user: nacos
password: nacos
dataid: user-web.json
group: dev
04-阶段四:微服务实现电商系统
第11周 商品微服务的grpc服务
第1章商品服务-service服务
1-1需求分析-数据库实体分析
1-2需求分析-商品微服务接口分析
1-3商品分类表结构设计应该注意什么?
// goods_srv/model/goods.go
package model
type Category struct {
BaseModel
// 尽量不要让字段为null
Name string `gorm:"type:varchar(20);not null"`
// 外键id
ParentCategoryID int32
// 父级分类
ParentCategory *Category
// Level 分类级别(一级二级三级)
Level int32 `gorm:"type:int;not null;default:1"`
// IsTab 是否展示在首页tab栏
IsTab bool `gorm:"default:false;not null"`
}
// goods_srv/model/base.go
package model
import (
"gorm.io/gorm"
"time"
)
type BaseModel struct {
// 用自己的model(而不是gorm的)方便扩展
ID int32 `gorm:"primarykey;type:int"`
CreatedAt time.Time `gorm:"column:add_time"`
UpdatedAt time.Time `gorm:"column:update_time"`
DeletedAt gorm.DeletedAt
//IsDeleted bool `gorm:"column:is_deleted"`
IsDeleted bool
}
1-4品牌、轮播图表结构设计
// goods_srv/model/goods.go
package model
// ...
// Brands 品牌
type Brands struct {
BaseModel
Name string `gorm:"type:varchar(20);not null"`
Logo string `gorm:"type:varchar(200);default:'';not null"`
}
// GoodsCategoryBrand 品牌商品分类对应表
type GoodsCategoryBrand struct {
BaseModel
// 如果两个字段指定的index一样, 会成为联合的唯一索引
CategoryID int32 `gorm:"type:int;index:idx_category_brand,unique"`
Category Category
BrandsID int32 `gorm:"type:int;index:idx_category_brand,unique"`
Brands Brands
}
// 重载表名(gorm自动创建为x_x_x的表名)
func (GoodsCategoryBrand) TableName() string {
return "goodscategorybrand"
}
// Banner 广告轮播图
type Banner struct {
BaseModel
Image string `gorm:"type:varchar(200);not null"`
// 点击后跳转的url
Url string `gorm:"type:varchar(200);not null"`
Index int32 `gorm:"type:int;default:1;not null"`
}
1-5商品表结构设计
// goods_srv/model/goods.go
// ...
// Goods 商品表
type Goods struct {
BaseModel
CategoryID int32 `gorm:"type:int;not null"`
Category Category
BrandsID int32 `gorm:"type:int;not null"`
Brands Brands
// 是否上架
OnSale bool `gorm:"default:false;not null"`
// 是否免运费
ShipFree bool `gorm:"default:false;not null"`
// 是否为新品
IsNew bool `gorm:"default:false;not null"`
// 是否热门商品
IsHot bool `gorm:"default:false;not null"`
Name string `gorm:"type:varchar(50);not null"`
// 商家指定的商品编号
GoodsSn string `gorm:"type:varchar(50);not null"`
// 商品点击数
ClickNum int32 `gorm:"type:int;default:0;not null"`
// 商品卖了多少件
SoldNum int32 `gorm:"type:int;default:0;not null"`
// 商品被收藏数
FavNum int32 `gorm:"type:int;default:0;not null"`
// 商品价格
MarketPrice float32 `gorm:"not null"`
// 实际价格
ShopPrice float32 `gorm:"not null"`
// 商品简介
GoodsBrief string `gorm:"type:varchar(100);not null"`
// 商品图片
//Images []string
Images GormList `gorm:"type:varchar(1000);not null"`
// 描述图片
DescImages GormList `gorm:"type:varchar(1000);not null"`
// 封面图
GoodsFrontImage string `gorm:"type:varchar(200);not null"`
}
// GoodsImages 商品图片表
// 如果数据量很大, 则join也是会销毁不少性能的
//type GoodsImages struct {
// GoodsID int
// Image string
//}
// goods_srv/model/base.go
package model
// ...
// gorm 自定义类型 -> 鸭子类型语言:
// 在程序设计中是动态类型的一种风格。在这种风格中,一个物件有效的语义,
// 不是由继承自特定的类或实现特定的接口,而是由“当前方法和属性的集合”决定。
type GormList []string
func (g GormList) Value() (driver.Value, error) {
return json.Marshal(g)
}
// 数据库中的值映射到go
func (g *GormList) Scan(value interface{}) error {
return json.Unmarshal(value.([]byte), &g)
}
1-6生成表结构和导入数据
// goods_srv/model/main/main.go
package main
import (
_ "github.com/anaskhan96/go-password-encoder"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"gorm.io/gorm/schema"
"log"
"mxshop_srvs/goods_srv/model"
"os"
"time"
)
func main() {
dsn := "root:123456@tcp(127.0.0.1:3307)/mxshop_goods_srv?charset=utf8mb4&parseTime=True&loc=Local"
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
logger.Config{
SlowThreshold: time.Second, // 慢 SQL 阈值
LogLevel: logger.Info,
Colorful: true, // 禁用彩色打印
},
)
// 全局模式
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
NamingStrategy: schema.NamingStrategy{
// 为false会根据 结构体名+s 作为表名
SingularTable: true,
},
Logger: newLogger,
})
if err != nil {
panic(err)
}
// 定义一个表结构, 生成对应表
_ = db.AutoMigrate(
&model.Category{},
&model.Brands{},
&model.GoodsCategoryBrand{},
&model.Banner{},
&model.Goods{},
)
}
1-7定义proto接口
1-8快速启动grpc服务
// goods_srv/main.go
package main
import (
"flag"
"fmt"
"github.com/hashicorp/consul/api"
"github.com/satori/go.uuid"
"go.uber.org/zap"
"google.golang.org/grpc"
"google.golang.org/grpc/health"
"google.golang.org/grpc/health/grpc_health_v1"
"mxshop_srvs/goods_srv/global"
"mxshop_srvs/goods_srv/handler"
"mxshop_srvs/goods_srv/initialize"
"mxshop_srvs/goods_srv/proto"
"mxshop_srvs/goods_srv/utils"
"net"
"os"
"os/signal"
"syscall"
)
func main() {
// 让用户从命令行传递
//IP := flag.String("ip", "0.0.0.0", "ip地址")
IP := flag.String("ip", "0.0.0.0", "ip地址")
Port := flag.Int("port", 0, "端口号")
// 初始化日志
initialize.InitLogger()
initialize.InitConfig()
initialize.InitDB()
zap.S().Info(global.ServerConfig)
flag.Parse()
zap.S().Info("ip: ", *IP)
if *Port == 0 {
*Port, _ = utils.GetFreePort()
}
zap.S().Info("port: ", *Port)
server := grpc.NewServer()
proto.RegisterGoodsServer(server, &handler.GoodsServer{})
lis, err := net.Listen("tcp", fmt.Sprintf("%s:%d", *IP, *Port))
if err != nil {
panic("failed to listen: " + err.Error())
}
// 注册服务健康检查
grpc_health_v1.RegisterHealthServer(server, health.NewServer())
// 服务注册
cfg := api.DefaultConfig()
sc := global.ServerConfig
cfg.Address = fmt.Sprintf("%s:%d", sc.ConsulInfo.Host, sc.ConsulInfo.Port)
cli, err := api.NewClient(cfg)
if err != nil {
panic(err)
}
// 生成对应的检查对象
check := &api.AgentServiceCheck{
// 0.0.0.0可以用于监听,但是不能用来访问(比如consul进行健康检测)
GRPC: fmt.Sprintf("%s:%d", global.ServerConfig.Host, *Port),
Timeout: "5s",
Interval: "5s",
DeregisterCriticalServiceAfter: "15s",
}
// 创建注册对象
registration := new(api.AgentServiceRegistration)
registration.Name = sc.Name
// 名称相同id不同, consul就不会覆盖
serviceID := fmt.Sprintf("%s", uuid.NewV4())
//registration.ID = sc.Name
registration.ID = serviceID
registration.Port = *Port
registration.Tags = global.ServerConfig.Tags
registration.Address = global.ServerConfig.Host
registration.Check = check
err = cli.Agent().ServiceRegister(registration)
if err != nil {
return
}
go func() {
// 这里是阻塞代码, 后面的执行不到, 所以改造为异步
err = server.Serve(lis)
if err != nil {
panic("failed to start grpc: " + err.Error())
}
}()
// 接收终止信号, 注销服务
quit := make(chan os.Signal)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
if err = cli.Agent().ServiceDeregister(serviceID); err != nil {
zap.S().Info("注销失败")
panic(err)
}
zap.S().Info("注销成功")
}
// goods_srv/handler/goods.go
package handler
import (
"mxshop_srvs/goods_srv/proto"
)
type GoodsServer struct {
proto.UnimplementedGoodsServer
}
host: 127.0.0.1
port: 8848
namespace: 8cb36ec9-f281-42b5-b354-44c1c1cb4008
user: nacos
password: nacos
dataid: goods_srv.json
group: dev
// nacos -> goods_srv.json
{
"name": "goods_srv",
"host": "127.0.0.1",
"tags": [
"imooc",
"goods",
"srv"
],
"consul": {
"host": "127.0.0.1",
"port": 8500
},
"mysql": {
"host": "127.0.0.1",
"port": 3307,
"user": "root",
"password": "123456",
"db": "mxshop_goods_srv"
}
}
// goods_srv/initialize/config.go
package initialize
import (
"encoding/json"
"fmt"
"github.com/nacos-group/nacos-sdk-go/v2/clients"
"github.com/nacos-group/nacos-sdk-go/v2/common/constant"
"github.com/nacos-group/nacos-sdk-go/v2/vo"
"github.com/spf13/viper"
"go.uber.org/zap"
"mxshop_srvs/goods_srv/global"
)
func GetEnvInfo(env string) bool {
viper.AutomaticEnv()
// 设置的环境变量要生效, 要重启ide(不要用缓存清除重启)
return viper.GetBool(env)
}
// InitConfig 从配置文件读取配置
func InitConfig() {
debug := GetEnvInfo("MXSHOP_DEBUG")
configFilePrefix := "config"
configFileName := fmt.Sprintf("goods_srv/%s-product.yaml", configFilePrefix)
// 线上线下配置隔离
if debug {
configFileName = fmt.Sprintf("goods_srv/%s-debug.yaml", configFilePrefix)
}
v := viper.New()
v.SetConfigFile(configFileName)
if err := v.ReadInConfig(); err != nil {
panic(err)
}
if err := v.Unmarshal(&global.NacosConfig); err != nil {
panic(err)
}
// 从nacos获取配置
sc := []constant.ServerConfig{
{
IpAddr: global.NacosConfig.Host,
ContextPath: "/nacos",
Port: global.NacosConfig.Port,
Scheme: "http",
},
}
cc := constant.ClientConfig{
NamespaceId: global.NacosConfig.Namespace,
TimeoutMs: 5000,
NotLoadCacheAtStart: true,
LogDir: "tmp/nacos/log",
// 网络中获取不到, 会从缓存拿
CacheDir: "tmp/nacos/cache",
LogLevel: "debug",
}
// 创建动态配置客户端的另一种方式 (推荐)
configClient, err := clients.NewConfigClient(
vo.NacosClientParam{
ClientConfig: &cc,
ServerConfigs: sc,
},
)
if err != nil {
zap.S().Fatalf("nacos创建客户端失败: %s", err.Error())
return
}
// 获取配置
content, err := configClient.GetConfig(
vo.ConfigParam{
DataId: global.NacosConfig.Dataid,
Group: global.NacosConfig.Group,
},
)
if err != nil {
zap.S().Fatalf("读取nacos配置失败: %s", err.Error())
return
}
// 转换为json需要tag
err = json.Unmarshal([]byte(content), &global.ServerConfig)
if err != nil {
zap.S().Fatalf("nacos配置转换struct失败: %s", err.Error())
return
}
// 监听配置变化 (好像不是很必要)
err = configClient.ListenConfig(vo.ConfigParam{
DataId: global.NacosConfig.Dataid,
Group: global.NacosConfig.Group,
OnChange: func(namespace, group, dataId, data string) {
//zap.S().Infof("配置文件变化: \n%s\n", data)
zap.S().Info("配置文件变化")
// 转换为json需要tag
err = json.Unmarshal([]byte(data), &global.ServerConfig)
if err != nil {
zap.S().Fatalf("nacos配置转换struct失败: %s", err.Error())
return
}
zap.S().Debugf("当前配置: \n%v\n", global.ServerConfig)
},
})
if err != nil {
panic(err)
}
}
// goods_srv/global/global.go
package global
import (
"gorm.io/gorm"
"mxshop_srvs/goods_srv/config"
)
var (
NacosConfig *config.NacosConfig = &config.NacosConfig{}
DB *gorm.DB
ServerConfig *config.ServerConfig = &config.ServerConfig{}
)
// goods_srv/config/config.go
package config
type MysqlConfig struct {
Host string `mapstructure:"host" json:"host"`
Port int `mapstructure:"port" json:"port"`
Name string `mapstructure:"db" json:"db"`
User string `mapstructure:"user" json:"user"`
Password string `mapstructure:"password" json:"password"`
}
type ConsulConfig struct {
Host string `mapstructure:"host" json:"host"`
Port int `mapstructure:"port" json:"port"`
}
type ServerConfig struct {
Host string `mapstructure:"host" json:"host"`
//Port int `mapstructure:"port" json:"port"`
// 用于服务注册
Name string `mapstructure:"name" json:"name"`
Tags []string `mapstructure:"tags" json:"tags"`
MysqlInfo MysqlConfig `mapstructure:"mysql" json:"mysql"`
ConsulInfo MysqlConfig `mapstructure:"consul" json:"consul"`
}
type NacosConfig struct {
Host string `mapstructure:"host"`
Port uint64 `mapstructure:"port"`
Namespace string `mapstructure:"namespace"`
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
Dataid string `mapstructure:"dataid"`
Group string `mapstructure:"group"`
}
1-9品牌列表实现
// goods_srv/test/brands/main.go
package main
import (
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"mxshop_srvs/goods_srv/proto"
)
var bc proto.GoodsClient
var conn *grpc.ClientConn
func Init() {
var err error
conn, err = grpc.Dial(
fmt.Sprintf("127.0.0.1:50051"),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
panic(err)
}
bc = proto.NewGoodsClient(conn)
}
func TestGetBrandList() {
rsp, err := bc.BrandList(context.Background(), &proto.BrandFilterRequest{
Pages: 0,
PagePerNums: 6,
})
if err != nil {
panic(err)
}
fmt.Println(rsp.Total)
for _, u := range rsp.Data {
fmt.Println(u)
}
}
func main() {
Init()
TestGetBrandList()
defer conn.Close()
}
// goods_srv/handler/brands.go
package handler
import (
"context"
"mxshop_srvs/goods_srv/global"
"mxshop_srvs/goods_srv/model"
"mxshop_srvs/goods_srv/proto"
)
func (g GoodsServer) BrandList(ctx context.Context, req *proto.BrandFilterRequest) (*proto.BrandListResponse, error) {
blr := proto.BrandListResponse{}
var brands []model.Brands
res := global.DB.Scopes(Paginate(int(req.Pages), int(req.PagePerNums))).Find(&brands)
if res.Error != nil {
return nil, res.Error
}
// 品牌总数让前端用于分页, 而不是当前分页查询出来的条数
var count int64
global.DB.Model(&model.Brands{}).Count(&count)
//blr.Total = int32(res.RowsAffected)
blr.Total = int32(count)
var brandRes []*proto.BrandInfoResponse
for _, b := range brands {
brandRes = append(brandRes, &proto.BrandInfoResponse{
Id: b.ID,
Name: b.Name,
Logo: b.Logo,
})
}
blr.Data = brandRes
return &blr, nil
}
// goods_srv/handler/base.go
package handler
import "gorm.io/gorm"
// Paginate 优雅分页
func Paginate(page, pageSize int) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
if page <= 0 {
page = 1
}
switch {
case pageSize > 100:
pageSize = 100
case pageSize <= 0:
pageSize = 10
}
offset := (page - 1) * pageSize
return db.Offset(offset).Limit(pageSize)
}
}
1-10品牌新建,删除、更新
// goods_srv/handler/brands.go
package handler
import (
"context"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
"mxshop_srvs/goods_srv/global"
"mxshop_srvs/goods_srv/model"
"mxshop_srvs/goods_srv/proto"
)
func (g GoodsServer) BrandList(ctx context.Context, req *proto.BrandFilterRequest) (*proto.BrandListResponse, error) {
blr := proto.BrandListResponse{}
var brands []model.Brands
res := global.DB.Scopes(Paginate(int(req.Pages), int(req.PagePerNums))).Find(&brands)
if res.Error != nil {
return nil, res.Error
}
// 品牌总数让前端用于分页, 而不是当前分页查询出来的条数
var count int64
global.DB.Model(&model.Brands{}).Count(&count)
//blr.Total = int32(res.RowsAffected)
blr.Total = int32(count)
var brandRes []*proto.BrandInfoResponse
for _, b := range brands {
brandRes = append(brandRes, &proto.BrandInfoResponse{
Id: b.ID,
Name: b.Name,
Logo: b.Logo,
})
}
blr.Data = brandRes
return &blr, nil
}
// CreateBrand 新建品牌
func (g GoodsServer) CreateBrand(ctx context.Context, req *proto.BrandRequest) (*proto.BrandInfoResponse, error) {
// 靠品牌名称判断是否重复
if res := global.DB.Where("name = ?", req.Name).First(&model.Brands{}); res.RowsAffected > 0 {
return nil, status.Errorf(codes.InvalidArgument, "品牌已存在")
}
brand := &model.Brands{
Name: req.Name,
Logo: req.Logo,
}
global.DB.Save(brand)
return &proto.BrandInfoResponse{
Id: brand.ID,
}, nil
}
func (g GoodsServer) DeleteBrand(ctx context.Context, req *proto.BrandRequest) (*emptypb.Empty, error) {
if res := global.DB.Delete(&model.Brands{}, req.Id); res.RowsAffected == 0 {
return nil, status.Errorf(codes.NotFound, "品牌不存在")
}
return &emptypb.Empty{}, nil
}
func (g GoodsServer) UpdateBrand(ctx context.Context, req *proto.BrandRequest) (*emptypb.Empty, error) {
brand := model.Brands{}
brand.ID = req.Id
if res := global.DB.First(&brand); res.RowsAffected == 0 {
return nil, status.Errorf(codes.NotFound, "品牌不存在")
}
if req.Name != "" {
brand.Name = req.Name
}
if req.Logo != "" {
brand.Logo = req.Logo
}
if res := global.DB.Save(&brand); res.RowsAffected == 0 {
return nil, status.Errorf(codes.NotFound, "品牌不存在")
}
return &emptypb.Empty{}, nil
}
// goods_srv/test/brands/main.go
package main
import (
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"mxshop_srvs/goods_srv/proto"
)
var bc proto.GoodsClient
var conn *grpc.ClientConn
func Init() {
var err error
conn, err = grpc.Dial(
fmt.Sprintf("127.0.0.1:50051"),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
panic(err)
}
bc = proto.NewGoodsClient(conn)
}
func TestGetBrandList() {
rsp, err := bc.BrandList(context.Background(), &proto.BrandFilterRequest{
Pages: 0,
PagePerNums: 6,
})
if err != nil {
panic(err)
}
fmt.Println(rsp.Total)
for _, u := range rsp.Data {
fmt.Println(u)
}
}
func TestCreateBrand() {
brand, err := bc.CreateBrand(context.Background(), &proto.BrandRequest{
Name: "???4",
Logo: "https://img30.360buyimg.com/popshop/jfs/t3148/339/6218763594/13705/2efaf19e/58a17f5fNf29a51cd.jpg",
})
if err != nil {
panic(err)
}
fmt.Println(brand)
}
func TestUpdateBrand() {
_, err := bc.UpdateBrand(context.Background(), &proto.BrandRequest{
Id: 1119,
//Id: 2000,
Name: "fffff",
//Logo: "https://img30.360buyimg.com/popshop/jfs/t3148/339/6218763594/13705/2efaf19e/58a17f5fNf29a51cd.jpg",
})
if err != nil {
panic(err)
}
}
func TestDeleteBrand() {
_, err := bc.DeleteBrand(context.Background(), &proto.BrandRequest{
Id: 1113,
//Id: 2000,
})
if err != nil {
panic(err)
}
}
func main() {
Init()
//TestGetBrandList()
//TestCreateBrand()
//TestUpdateBrand()
//TestDeleteBrand()
defer conn.Close()
}
1-11轮播图的查询、新增、删除和修改
// goods_srv/handler/banner.go
package handler
import (
"context"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
"mxshop_srvs/goods_srv/model"
"mxshop_srvs/goods_srv/proto"
"mxshop_srvs/goods_srv/global"
)
func (g GoodsServer) BannerList(ctx context.Context, empty *emptypb.Empty) (*proto.BannerListResponse, error) {
res := proto.BannerListResponse{}
var bs []model.Banner
result := global.DB.Find(&bs)
// 轮播图没几个就不需要分页
res.Total = int32(result.RowsAffected)
var brs []*proto.BannerResponse
for _, ban := range bs {
brs = append(brs, &proto.BannerResponse{
Id: ban.ID,
Index: ban.Index,
Image: ban.Image,
Url: ban.Url,
})
}
res.Data = brs
return &res, nil
}
func (g GoodsServer) CreateBanner(ctx context.Context, req *proto.BannerRequest) (*proto.BannerResponse, error) {
b := model.Banner{}
b.Image = req.Image
b.Index = req.Index
b.Url = req.Url
global.DB.Save(&b)
return &proto.BannerResponse{
Id: b.ID,
}, nil
}
func (g GoodsServer) DeleteBanner(ctx context.Context, req *proto.BannerRequest) (*emptypb.Empty, error) {
if res := global.DB.Delete(&model.Banner{}, req.Id); res.RowsAffected == 0 {
return nil, status.Errorf(codes.NotFound, "轮播图不存在")
}
return &emptypb.Empty{}, nil
}
func (g GoodsServer) UpdateBanner(ctx context.Context, req *proto.BannerRequest) (*emptypb.Empty, error) {
var ban model.Banner
if res := global.DB.First(&ban, req.Id); res.RowsAffected == 0 {
return nil, status.Errorf(codes.NotFound, "轮播图不存在")
}
if req.Url != "" {
ban.Url = req.Url
}
if req.Image != "" {
ban.Image = req.Image
}
if req.Index != 0 {
ban.Index = req.Index
}
global.DB.Save(&ban)
return &emptypb.Empty{}, nil
}
package main
import (
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/protobuf/types/known/emptypb"
"mxshop_srvs/goods_srv/proto"
)
var bc proto.GoodsClient
var conn *grpc.ClientConn
func Init() {
var err error
conn, err = grpc.Dial(
fmt.Sprintf("127.0.0.1:50051"),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
panic(err)
}
bc = proto.NewGoodsClient(conn)
}
func TestGetBannerList() {
list, err := bc.BannerList(context.Background(), &emptypb.Empty{})
if err != nil {
panic(err)
}
fmt.Println(list.Total)
for _, item := range list.Data {
fmt.Println(item)
}
}
func TestCreateBanner() *proto.BannerResponse {
banner, err := bc.CreateBanner(context.Background(), &proto.BannerRequest{
Index: 0,
Image: "http://shop.projectsedu.com/media/banner/banner1_qlGlwc9.jpg",
Url: "427",
})
if err != nil {
panic(err)
}
fmt.Println(banner)
return banner
}
func TestUptBanner(id int32) {
b, err := bc.UpdateBanner(context.Background(), &proto.BannerRequest{
Id: id,
Url: "500",
})
if err != nil {
panic(err)
}
fmt.Println(b)
}
func TestDelBanner(id int32) {
_, err := bc.DeleteBanner(context.Background(), &proto.BannerRequest{
Id: id,
})
if err != nil {
panic(err)
}
}
func main() {
Init()
TestGetBannerList()
//banner := TestCreateBanner()
//id := banner.Id
//TestUptBanner(id)
//TestDelBanner(id)
conn.Close()
}
1-12商品分类的列表接口
// goods_srv/model/goods.go
package model
// Category 分类
type Category struct {
BaseModel
// 尽量不要让字段为null
Name string `gorm:"type:varchar(20);not null" json:"name"`
// 外键id
ParentCategoryID int32 `json:"parent"`
// 父级分类
ParentCategory *Category `json:"-"` // - 忽略json序列化
// 子分类 -> foreignKey: 外键; references: 外键指向的字段
SubCategory []*Category `gorm:"foreignKey:ParentCategoryID;references:ID" json:"sub_category"`
// Level 分类级别(一级二级三级)
Level int32 `gorm:"type:int;not null;default:1" json:"level"`
// IsTab 是否展示在首页tab栏
IsTab bool `gorm:"default:false;not null" json:"is_tab"`
}
// ...
// goods_srv/model/base.go
package model
import (
"database/sql/driver"
"encoding/json"
"gorm.io/gorm"
"time"
)
type BaseModel struct {
// 用自己的model(而不是gorm的)方便扩展
ID int32 `gorm:"primarykey;type:int" json:"id"`
CreatedAt time.Time `gorm:"column:add_time" json:"-"`
UpdatedAt time.Time `gorm:"column:update_time" json:"-"`
DeletedAt gorm.DeletedAt `json:"-"`
//IsDeleted bool `gorm:"column:is_deleted"`
IsDeleted bool `json:"-"`
}
// goods_srv/test/handler/category/main.go
package main
import (
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/protobuf/types/known/emptypb"
"mxshop_srvs/goods_srv/proto"
)
var bc proto.GoodsClient
var conn *grpc.ClientConn
func Init() {
var err error
conn, err = grpc.Dial(
fmt.Sprintf("127.0.0.1:50051"),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
panic(err)
}
bc = proto.NewGoodsClient(conn)
}
func TestGetAllCategory() {
res, err := bc.GetAllCategorysList(context.Background(), &emptypb.Empty{})
if err != nil {
panic(err)
}
fmt.Println(res.Total)
fmt.Println(res.JsonData)
}
func main() {
Init()
TestGetAllCategory()
conn.Close()
}
// goods_srv/handler/category.go
package handler
import (
"context"
"encoding/json"
"google.golang.org/protobuf/types/known/emptypb"
"mxshop_srvs/goods_srv/global"
"mxshop_srvs/goods_srv/model"
"mxshop_srvs/goods_srv/proto"
)
// GetAllCategorysList 获取所有分类
func (g GoodsServer) GetAllCategorysList(ctx context.Context, empty *emptypb.Empty) (*proto.CategoryListResponse, error) {
var categorys []model.Category
global.DB.
Where(&model.Category{Level: 1}).
// 加载子分类
Preload("SubCategory.SubCategory").
Find(&categorys)
b, _ := json.Marshal(&categorys)
return &proto.CategoryListResponse{
JsonData: string(b),
}, nil
}
1-14获取商品分类的子分类
// goods_srv/handler/category.go
// ...
// GetSubCategory 获取子分类
func (g GoodsServer) GetSubCategory(ctx context.Context, req *proto.CategoryListRequest) (*proto.SubCategoryListResponse, error) {
categoryListRes := proto.SubCategoryListResponse{}
var category model.Category
if res := global.DB.First(&category, req.Id); res.RowsAffected == 0 {
return nil, status.Errorf(codes.NotFound, "商品分类不存在")
}
categoryListRes.Info = &proto.CategoryInfoResponse{
Id: category.ID,
Name: category.Name,
Level: category.Level,
IsTab: category.IsTab,
ParentCategory: category.ParentCategoryID,
}
var subCategorys []model.Category
// 如果为1级则需要查循两层level的子分类
//preloads := "SubCategory"
//if category.Level == 1 {
// preloads = "SubCategory.SubCategory"
//}
global.DB.
Where(&model.Category{ParentCategoryID: req.Id}).
// 所以其实不需要preload
//Preload(preloads).
Find(&subCategorys)
var subCategoryRes []*proto.CategoryInfoResponse
for _, sc := range subCategorys {
subCategoryRes = append(subCategoryRes, &proto.CategoryInfoResponse{
Id: sc.ID,
Name: sc.Name,
ParentCategory: sc.ParentCategoryID,
Level: sc.Level,
IsTab: sc.IsTab,
})
}
categoryListRes.SubCategorys = subCategoryRes
return &categoryListRes, nil
}
// goods_srv/test/handler/category/main.go
package main
// ...
func TestGetSubCategory(id int) {
category, err := bc.GetSubCategory(context.Background(), &proto.CategoryListRequest{
Id: int32(id),
})
if err != nil {
panic(err)
}
fmt.Println(category.Info)
for _, v := range category.SubCategorys {
fmt.Println(v)
}
}
func main() {
Init()
TestGetAllCategory()
TestGetSubCategory(130364) // 2
TestGetSubCategory(130358) // 1
conn.Close()
}
1-15商品分类的新建,删除和更新接口
// goods_srv/handler/category.go
// ...
func (g GoodsServer) CreateCategory(ctx context.Context, req *proto.CategoryInfoRequest) (*proto.CategoryInfoResponse, error) {
category := model.Category{}
category.Name = req.Name
category.Level = req.Level
// todo 如果没传ParentCategory无法创建 -> err
if req.Level != 1 {
category.ParentCategoryID = req.ParentCategory
}
category.IsTab = req.IsTab
global.DB.Save(&category)
return &proto.CategoryInfoResponse{
Id: category.ID,
}, nil
}
func (g GoodsServer) DeleteCategory(ctx context.Context, req *proto.DeleteCategoryRequest) (*emptypb.Empty, error) {
if res := global.DB.Delete(&model.Category{}, req.Id); res.RowsAffected == 0 {
return nil, status.Errorf(codes.NotFound, "商品分类不存在")
}
return &emptypb.Empty{}, nil
}
func (g GoodsServer) UpdateCategory(ctx context.Context, req *proto.CategoryInfoRequest) (*emptypb.Empty, error) {
var category model.Category
if res := global.DB.First(&category, req.Id); res.RowsAffected == 0 {
return nil, status.Errorf(codes.NotFound, "商品分类不存在")
}
if req.Name != "" {
category.Name = req.Name
}
if req.ParentCategory != 0 {
category.ParentCategoryID = req.ParentCategory
}
if req.Level != 0 {
category.Level = req.Level
}
if req.IsTab {
category.IsTab = req.IsTab
}
global.DB.Save(&category)
return &emptypb.Empty{}, nil
}
1-16品牌分类相关接口
// goods_srv/test/handler/category_brand/main.go
package main
import (
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"mxshop_srvs/goods_srv/proto"
)
var bc proto.GoodsClient
var conn *grpc.ClientConn
func Init() {
var err error
conn, err = grpc.Dial(
fmt.Sprintf("127.0.0.1:50051"),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
panic(err)
}
bc = proto.NewGoodsClient(conn)
}
func TestCategoryBrandList() {
list, err := bc.CategoryBrandList(context.Background(), &proto.CategoryBrandFilterRequest{
Pages: 0,
PagePerNums: 5,
})
if err != nil {
panic(err)
}
fmt.Println(list.Total)
for _, v := range list.Data {
fmt.Println(v)
}
}
func TestGetCategoryBrandList() {
rsp, err := bc.GetCategoryBrandList(context.Background(), &proto.CategoryInfoRequest{
Id: 135475,
})
if err != nil {
panic(err)
}
fmt.Println(rsp.Total)
fmt.Println(rsp.Data)
}
func TestCreateCategoryBrand() int32 {
res, err := bc.CreateCategoryBrand(context.Background(), &proto.CategoryBrandRequest{
CategoryId: 130368,
BrandId: 922,
//BrandId: 345246,
})
if err != nil {
panic(err)
}
fmt.Println(res)
return res.Id
}
func TestUpdateCategoryBrand(id int32) {
_, err := bc.UpdateCategoryBrand(context.Background(), &proto.CategoryBrandRequest{
Id: id,
CategoryId: 135476,
BrandId: 634,
})
if err != nil {
panic(err)
}
}
func TestDeleteCategoryBrand(id int32) {
_, err := bc.DeleteCategoryBrand(context.Background(), &proto.CategoryBrandRequest{
Id: id,
})
if err != nil {
panic(err)
}
}
func main() {
Init()
TestCategoryBrandList()
TestGetCategoryBrandList()
id := TestCreateCategoryBrand()
TestUpdateCategoryBrand(id)
TestDeleteCategoryBrand(id)
defer conn.Close()
}
// goods_srv/handler/brands.go
package handler
import (
"context"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
"mxshop_srvs/goods_srv/global"
"mxshop_srvs/goods_srv/model"
"mxshop_srvs/goods_srv/proto"
)
func (g GoodsServer) CategoryBrandList(ctx context.Context, req *proto.CategoryBrandFilterRequest) (*proto.CategoryBrandListResponse, error) {
categoryBrandListResponse := proto.CategoryBrandListResponse{}
var total int64
global.DB.Model(&model.GoodsCategoryBrand{}).Count(&total)
categoryBrandListResponse.Total = int32(total)
var categoryBrands []model.GoodsCategoryBrand
// 需要preload才能把外键对应的model的值得到
global.DB.
Preload("Category").
Preload("Brands").
Scopes(Paginate(int(req.Pages), int(req.PagePerNums))).
Find(&categoryBrands)
var categoryResponses []*proto.CategoryBrandResponse
for _, cb := range categoryBrands {
categoryResponses = append(categoryResponses, &proto.CategoryBrandResponse{
Brand: &proto.BrandInfoResponse{
Id: cb.Brands.ID,
Name: cb.Brands.Name,
Logo: cb.Brands.Logo,
},
Category: &proto.CategoryInfoResponse{
Id: cb.Category.ID,
Name: cb.Category.Name,
ParentCategory: cb.Category.ParentCategoryID,
Level: cb.Category.Level,
IsTab: cb.Category.IsTab,
},
})
}
categoryBrandListResponse.Data = categoryResponses
return &categoryBrandListResponse, nil
}
// GetCategoryBrandList 根据商品分类查询brand
func (g GoodsServer) GetCategoryBrandList(ctx context.Context, req *proto.CategoryInfoRequest) (*proto.BrandListResponse, error) {
brandListResponse := proto.BrandListResponse{}
var category model.Category
if res := global.DB.Find(&category, req.Id).First(&category); res.RowsAffected == 0 {
return nil, status.Errorf(codes.InvalidArgument, "商品分类不存在")
}
var categoryBrands []model.GoodsCategoryBrand
if res := global.DB.
Preload("Brands").
Where(&model.GoodsCategoryBrand{CategoryID: category.ID}).
Find(&categoryBrands); res.RowsAffected > 0 {
brandListResponse.Total = int32(res.RowsAffected)
}
var brandInfoResponses []*proto.BrandInfoResponse
for _, cb := range categoryBrands {
brandInfoResponses = append(brandInfoResponses, &proto.BrandInfoResponse{
Id: cb.Brands.ID,
Name: cb.Brands.Name,
Logo: cb.Brands.Logo,
})
}
brandListResponse.Data = brandInfoResponses
return &brandListResponse, nil
}
// CreateCategoryBrand 创建分类和品牌的对应
func (g GoodsServer) CreateCategoryBrand(ctx context.Context, req *proto.CategoryBrandRequest) (*proto.CategoryBrandResponse, error) {
var category model.Category
if res := global.DB.First(&category, req.CategoryId); res.RowsAffected == 0 {
return nil, status.Errorf(codes.InvalidArgument, "商品分类不存在")
}
var brand model.Brands
if res := global.DB.First(&brand, req.BrandId); res.RowsAffected == 0 {
return nil, status.Errorf(codes.InvalidArgument, "品牌不存在")
}
categoryBrand := model.GoodsCategoryBrand{
CategoryID: req.CategoryId,
BrandsID: req.BrandId,
}
global.DB.Save(&categoryBrand)
return &proto.CategoryBrandResponse{Id: categoryBrand.ID}, nil
}
func (g GoodsServer) DeleteCategoryBrand(ctx context.Context, req *proto.CategoryBrandRequest) (*emptypb.Empty, error) {
if res := global.DB.First(&model.GoodsCategoryBrand{}, req.Id); res.RowsAffected == 0 {
return &emptypb.Empty{}, status.Errorf(codes.InvalidArgument, "品牌分类不存在")
}
if res := global.DB.Delete(&model.GoodsCategoryBrand{}, req.Id); res.RowsAffected == 0 {
return &emptypb.Empty{}, status.Errorf(codes.Internal, "删除失败")
}
return &emptypb.Empty{}, nil
}
func (g GoodsServer) UpdateCategoryBrand(ctx context.Context, req *proto.CategoryBrandRequest) (*emptypb.Empty, error) {
var categoryBrand model.GoodsCategoryBrand
if res := global.DB.First(&categoryBrand, req.Id); res.RowsAffected == 0 {
return nil, status.Errorf(codes.InvalidArgument, "品牌分类不存在")
}
var category model.Category
if res := global.DB.First(&category, req.CategoryId); res.RowsAffected == 0 {
return nil, status.Errorf(codes.InvalidArgument, "商品分类不存在")
}
var brand model.Brands
if res := global.DB.First(&brand, req.BrandId); res.RowsAffected == 0 {
return nil, status.Errorf(codes.InvalidArgument, "品牌不存在")
}
categoryBrand.CategoryID = req.CategoryId
categoryBrand.BrandsID = req.BrandId
global.DB.Save(&categoryBrand)
return &emptypb.Empty{}, nil
}
1-17商品列表页接口
// goods_srv/handler/goods.go
package handler
import (
"context"
"fmt"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"mxshop_srvs/goods_srv/global"
"mxshop_srvs/goods_srv/model"
"mxshop_srvs/goods_srv/proto"
)
type GoodsServer struct {
proto.UnimplementedGoodsServer
}
func ModelToResponse(goods model.Goods) proto.GoodsInfoResponse {
return proto.GoodsInfoResponse{
Id: goods.ID,
CategoryId: goods.CategoryID,
Name: goods.Name,
GoodsSn: goods.GoodsSn,
ClickNum: goods.ClickNum,
SoldNum: goods.SoldNum,
FavNum: goods.FavNum,
MarketPrice: goods.MarketPrice,
ShopPrice: goods.ShopPrice,
GoodsBrief: goods.GoodsBrief,
ShipFree: goods.ShipFree,
GoodsFrontImage: goods.GoodsFrontImage,
IsNew: goods.IsNew,
IsHot: goods.IsHot,
OnSale: goods.OnSale,
DescImages: goods.DescImages,
Images: goods.Images,
Category: &proto.CategoryBriefInfoResponse{
Id: goods.Category.ID,
Name: goods.Category.Name,
},
Brand: &proto.BrandInfoResponse{
Id: goods.Brands.ID,
Name: goods.Brands.Name,
Logo: goods.Brands.Logo,
},
}
}
func (g GoodsServer) GoodsList(ctx context.Context, req *proto.GoodsFilterRequest) (*proto.GoodsListResponse, error) {
// 难点: 关键词搜索|查询新品|查询热门商品|价格区间查找商品|商品分类筛选 ...
goodsListResponse := &proto.GoodsListResponse{}
var goods []model.Goods
// 自定义db连接, 用于多次调用拼接多条件sql
localDB := global.DB.Model(model.Goods{})
// 关键词
if req.KeyWords != "" {
localDB = localDB.Where("name LIKE ?", "%"+req.KeyWords+"%")
}
// 热门
if req.IsHot {
localDB = localDB.Where(model.Goods{IsHot: true})
}
// 新商品
if req.IsNew {
localDB = localDB.Where("is_new=true")
}
// 价格区间
if req.PriceMin > 0 {
localDB = localDB.Where("shop_price >= ?", req.PriceMin)
}
if req.PriceMin > 0 {
localDB = localDB.Where("shop_price <= ?", req.PriceMax)
}
// 品牌
if req.Brand > 0 {
localDB = localDB.Where("brand_id = ?", req.Brand)
}
// 通过 category 查询
if req.TopCategory > 0 {
var category model.Category
if res := global.DB.First(&category, req.TopCategory); res.RowsAffected == 0 {
return nil, status.Errorf(codes.NotFound, "商品分类不存在")
}
var subQuery string
if category.Level == 1 {
subQuery = fmt.Sprintf("select id from category where parent_category_id in"+
" (select id from category where parent_category_id=%d)", req.TopCategory)
} else if category.Level == 2 {
subQuery = fmt.Sprintf("select id from category where parent_category_id=%d", req.TopCategory)
} else if category.Level == 3 {
subQuery = fmt.Sprintf("select id from category where id=%d", req.TopCategory)
}
localDB.Where(fmt.Sprintf("category_id in (%s)", subQuery))
}
var count int64
// 要在分页之前拿
localDB.Count(&count)
goodsListResponse.Total = int32(count)
if res := localDB.
Preload("Category").
Preload("Brands").
Scopes(Paginate(int(req.Pages), int(req.PagePerNums))).
Find(&goods); res.Error != nil {
return nil, res.Error
}
for _, good := range goods {
gir := ModelToResponse(good)
goodsListResponse.Data = append(goodsListResponse.Data, &gir)
}
return goodsListResponse, nil
}
// ...
// goods_srv/test/handler/goods/main.go
package main
import (
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"mxshop_srvs/goods_srv/proto"
)
var bc proto.GoodsClient
var conn *grpc.ClientConn
func Init() {
var err error
conn, err = grpc.Dial(
fmt.Sprintf("127.0.0.1:50051"),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
panic(err)
}
bc = proto.NewGoodsClient(conn)
}
func TestGoodsList() {
list, err := bc.GoodsList(context.Background(), &proto.GoodsFilterRequest{
PriceMin: 10,
PriceMax: 1000,
//IsHot: false,
//IsNew: false,
//IsTab: false,
//TopCategory: 130358,
TopCategory: 130361,
Pages: 0,
PagePerNums: 15,
//KeyWords: "深海速冻",
//Brand: 0,
})
if err != nil {
panic(err)
}
fmt.Println(list.Total)
for _, d := range list.Data {
fmt.Println(d)
}
}
func main() {
Init()
TestGoodsList()
defer conn.Close()
}
1-20批量获取商品信息、商品详情接口
// goods_srv/handler/goods.go
package handler
// ...
// BatchGetGoods 如果用户提交有多个商品的订单, 最终场景下需要批量获取商品消息
func (g GoodsServer) BatchGetGoods(ctx context.Context, info *proto.BatchGoodsIdInfo) (*proto.GoodsListResponse, error) {
GoodsListResponse := &proto.GoodsListResponse{}
var goods []model.Goods
//res := global.DB.Where("id in ?", info.Id).Find(&goods)
res := global.DB.Where(info.Id).Find(&goods)
if res.RowsAffected == 0 {
return nil, status.Errorf(codes.NotFound, "商品不存在")
}
for _, good := range goods {
goodsInfoRes := ModelToResponse(good)
GoodsListResponse.Data = append(GoodsListResponse.Data, &goodsInfoRes)
}
GoodsListResponse.Total = int32(res.RowsAffected)
return GoodsListResponse, nil
}
// GetGoodsDetail 获取商品详情
func (g GoodsServer) GetGoodsDetail(ctx context.Context, req *proto.GoodInfoRequest) (*proto.GoodsInfoResponse, error) {
var goods model.Goods
res := global.DB.First(&goods, req.Id)
if res.RowsAffected == 0 {
return nil, status.Errorf(codes.NotFound, "商品不存在")
}
goodsInfoRes := ModelToResponse(goods)
return &goodsInfoRes, nil
}
1-21新增、修改和删除商品接口
// goods_srv/handler/goods.go
// ...
func (g GoodsServer) CreateGoods(ctx context.Context, info *proto.CreateGoodsInfo) (*proto.GoodsInfoResponse, error) {
// 商品的品牌和分类必须存在
var category model.Category
if res := global.DB.First(&category, info.CategoryId); res.RowsAffected == 0 {
return nil, status.Errorf(codes.InvalidArgument, "商品分类不存在")
}
var brand model.Brands
if res := global.DB.First(&brand, info.Brand); res.RowsAffected == 0 {
return nil, status.Errorf(codes.InvalidArgument, "商品品牌不存在")
}
// 微服务中, 普通的文件上传不再使用 -> 使用第三方上传 -> images保存的是上传后返回的图片url
goods := model.Goods{
CategoryID: category.ID,
Category: category,
BrandsID: brand.ID,
Brands: brand,
OnSale: info.OnSale,
ShipFree: info.ShipFree,
IsNew: info.IsNew,
IsHot: info.IsHot,
Name: info.Name,
GoodsSn: info.GoodsSn,
MarketPrice: info.MarketPrice,
ShopPrice: info.ShopPrice,
GoodsBrief: info.GoodsBrief,
Images: info.Images,
DescImages: info.DescImages,
GoodsFrontImage: info.GoodsFrontImage,
}
global.DB.Save(&goods)
// return &proto.GoodsInfoResponse{
// Id: goods.ID,
// }, nil
return &proto.GoodsInfoResponse{
Id: goods.ID,
Name: goods.Name,
GoodsSn: goods.GoodsSn,
MarketPrice: goods.MarketPrice,
ShopPrice: goods.ShopPrice,
GoodsBrief: goods.GoodsBrief,
//GoodsDesc: goods.GoodsDesc,
// binding表单验证 bool类型如果不是指针可能出错
ShipFree: goods.ShipFree,
Images: goods.Images,
DescImages: goods.DescImages,
GoodsFrontImage: goods.GoodsFrontImage,
CategoryId: goods.CategoryID,
Brand: &proto.BrandInfoResponse{
Id: goods.BrandsID,
Name: goods.Brands.Name,
Logo: goods.Brands.Logo,
},
}, nil
}
func (g GoodsServer) DeleteGoods(ctx context.Context, info *proto.DeleteGoodsInfo) (*emptypb.Empty, error) {
if res := global.DB.Delete(&model.Goods{}, info.Id); res.RowsAffected == 0 {
return nil, status.Errorf(codes.NotFound, "商品不存在")
}
return &emptypb.Empty{}, nil
}
func (g GoodsServer) UpdateGoods(ctx context.Context, info *proto.CreateGoodsInfo) (*emptypb.Empty, error) {
var good model.Goods
if res := global.DB.First(&good, info.Id); res.RowsAffected == 0 {
return nil, status.Errorf(codes.NotFound, "商品不存在")
}
var category model.Category
if res := global.DB.First(&category, info.CategoryId); res.RowsAffected == 0 {
return nil, status.Errorf(codes.InvalidArgument, "商品分类不存在")
}
var brand model.Brands
if res := global.DB.First(&brand, info.Brand); res.RowsAffected == 0 {
return nil, status.Errorf(codes.InvalidArgument, "商品品牌不存在")
}
good.Brands = brand
good.BrandsID = brand.ID
good.Category = category
good.CategoryID = category.ID
good.Name = info.Name
good.GoodsSn = info.GoodsSn
good.MarketPrice = info.MarketPrice
good.ShopPrice = info.ShopPrice
good.GoodsBrief = info.GoodsBrief
good.ShipFree = info.ShipFree
good.Images = info.Images
good.DescImages = info.DescImages
good.GoodsFrontImage = info.GoodsFrontImage
good.IsNew = info.IsNew
good.IsHot = info.IsHot
good.OnSale = info.OnSale
global.DB.Save(&good)
return &emptypb.Empty{}, nil
}
第12周 商品微服务的gin层和oss图片服务
第12周 商品微服务的gin层和oss图片服务
第1章gin完成商品服务的http接口
1-1快速将用户的web服务转换成商品的web服务
复制user-web, 然后 编辑->查找->在文件中替换->goods-web目录下 替换user-web为goods-web
删除不需要的文件和方法, 修改global|config等…
host: 127.0.0.1
port: 8848
namespace: 8cb36ec9-f281-42b5-b354-44c1c1cb4008
user: nacos
password: nacos
dataid: goodss-web.json
group: dev
// goods-web/main.go
package main
import (
"fmt"
"github.com/spf13/viper"
"go.uber.org/zap"
"mxshop-api/goods-web/global"
"mxshop-api/goods-web/initialize"
"mxshop-api/goods-web/utils"
)
func main() {
// 1. 初始化logger
initialize.InitLogger()
// 初始化配置
initialize.InitConfig()
// 2. 初始化router
r := initialize.Routers()
// 初始化翻译
if err := initialize.InitTrans("zh"); err != nil {
panic(err)
}
// 初始化user_src连接(consul grpc)
initialize.InitSrvConn()
//port:=global.ServerConfig.Port
viper.AutomaticEnv()
// 本地开发环境端口固定, 线上环境自动获取端口号
debug := viper.GetBool("MXSHOP_DEBUG")
if !debug {
port, err := utils.GetFreePort()
if err == nil {
global.ServerConfig.Port = port
}
}
// 3. 运行接口
// zap.S() 直接拿到zap的suger
// S()和L()有加锁,并且使用的是全局logger,可以全局安全访问
zap.S().Infof("启动服务器, 端口: %d", global.ServerConfig.Port)
err := r.Run(fmt.Sprintf(":%d", global.ServerConfig.Port))
if err != nil {
zap.S().Panic("启动失败: ", err.Error())
}
}
// goods-web/api/goods.go
package goods
import (
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"mxshop-api/goods-web/global"
"net/http"
"strings"
)
func removeTopStruct(fields map[string]string) map[string]string {
rsp := map[string]string{}
for field, err := range fields {
rsp[field[strings.Index(field, ".")+1:]] = err
}
return rsp
}
// HandleGrpcErrorToHttp grpc的code转换为http状态码
func HandleGrpcErrorToHttp(err error, c *gin.Context) {
if err != nil {
if e, ok := status.FromError(err); ok {
switch e.Code() {
case codes.NotFound:
c.JSON(http.StatusNotFound, gin.H{
"msg": e.Message(),
})
case codes.Internal:
c.JSON(http.StatusInternalServerError, gin.H{
"msg": "内部错误",
})
case codes.InvalidArgument:
c.JSON(http.StatusBadRequest, gin.H{
"msg": "参数错误",
})
default:
c.JSON(http.StatusInternalServerError, gin.H{
//"msg": "其他错误: "+e.Message(),
"msg": "其他错误",
})
}
return
}
}
}
func HandleValidatorErr(c *gin.Context, err error) {
// 如何返回错误信息
errs, ok := err.(validator.ValidationErrors)
if !ok {
c.JSON(http.StatusOK, gin.H{
"msg": err.Error(),
})
}
c.JSON(http.StatusBadRequest, gin.H{
"error": removeTopStruct(errs.Translate(global.Trans)),
})
}
func List(ctx * gin.Context) {
}
// goods-web/router/goods.go
package router
import (
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"mxshop-api/goods-web/api/goods"
)
func InitGoodsRouter(group *gin.RouterGroup) {
gr := group.Group("goods")
zap.S().Info("配置用户相关的url")
{
gr.GET("list", goods.List)
}
}
// goods-web/initialize/router.go
package initialize
import (
"github.com/gin-gonic/gin"
"mxshop-api/goods-web/middlewares"
"mxshop-api/goods-web/router"
)
func Routers() *gin.Engine {
r := gin.Default()
// 跨域
r.Use(middlewares.Cors())
apiGroup := r.Group("/g/v1")
// 分组注册接口
router.InitGoodsRouter(apiGroup)
router.InitBaseRouter(apiGroup)
return r
}
// goods-web/config/config.go
package config
// grpc 服务端信息
type GoodsSrvConfig struct {
// 通过consul拿到服务 host port
Name string `mapstructure:"name" json:"name"`
}
type JWTConfig struct {
SigningKey string `mapstructure:"key" json:"key"`
}
type ConsulConfig struct {
Host string `mapstructure:"host" json:"host"`
Port int `mapstructure:"port" json:"port"`
}
type ServerConfig struct {
Name string `mapstructure:"name" json:"name"`
Port int `mapstructure:"port" json:"port"`
GoodsSrvInfo GoodsSrvConfig `mapstructure:"goods_srv" json:"goods_srv"`
JWTInfo JWTConfig `mapstructure:"jwt" json:"jwt"`
ConsulInfo ConsulConfig `mapstructure:"consul" json:"consul"`
}
type NacosConfig struct {
Host string `mapstructure:"host"`
Port uint64 `mapstructure:"port"`
Namespace string `mapstructure:"namespace"`
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
Dataid string `mapstructure:"dataid"`
Group string `mapstructure:"group"`
}
1-2商品的列表页接口
// goods-web.json
{
"consul": {
"host": "127.0.0.1",
"port": 8500
},
"jwt": {
"key": "3hTQH29YPr5oCcwatf1a1KbcnAft8Hn1"
},
"name": "goods-web",
"port": 8022,
"goods_srv": {
"name": "goods_srv"
}
}
// goods-web/api/goods.go
// ...
// 商品列表
func List(ctx *gin.Context) {
request := &proto.GoodsFilterRequest{}
priceMin := ctx.DefaultQuery("pmin", "0")
// 一般要考虑传递的是字符串怎么办 pmin=abc
// 不过这里转换失败会有默认值0, 不处理也没太大影响
priceMinInt, _ := strconv.Atoi(priceMin)
request.PriceMin = int32(priceMinInt)
priceMax := ctx.DefaultQuery("pmax", "0")
priceMaxInt, _ := strconv.Atoi(priceMax)
request.PriceMax = int32(priceMaxInt)
isHot := ctx.DefaultQuery("ih", "0")
if isHot == "1" {
request.IsHot = true
}
isNew := ctx.DefaultQuery("in", "0")
if isNew == "1" {
request.IsNew = true
}
isTab := ctx.DefaultQuery("it", "0")
if isTab == "1" {
request.IsTab = true
}
categoryId := ctx.DefaultQuery("c", "0")
categoryIdInt, _ := strconv.Atoi(categoryId)
request.TopCategory = int32(categoryIdInt)
pages := ctx.DefaultQuery("p", "0")
pagesInt, _ := strconv.Atoi(pages)
request.Pages = int32(pagesInt)
perNums := ctx.DefaultQuery("pnum", "0")
perNumsInt, _ := strconv.Atoi(perNums)
request.PagePerNums = int32(perNumsInt)
keywords := ctx.DefaultQuery("q", "")
request.KeyWords = keywords
brandId := ctx.DefaultQuery("b", "0")
brandIdInt, _ := strconv.Atoi(brandId)
request.Brand = int32(brandIdInt)
// 请求service服务
r, err := global.GoodsSrvClient.GoodsList(context.Background(), request)
if err != nil {
zap.S().Errorw("[List] 查询 [商品列表] 失败")
HandleGrpcErrorToHttp(err, ctx)
return
}
reMap := map[string]interface{}{
"total": r.Total,
//"data": r.Data,
}
goodsList := make([]interface{}, 0)
for _, value := range r.Data {
goodsList = append(goodsList, map[string]interface{}{
"id": value.Id,
"name": value.Name,
"goods_brief": value.GoodsBrief,
"desc": value.GoodsDesc,
"ship_free": value.ShipFree,
"images": value.Images,
"desc_images": value.DescImages,
"front_image": value.GoodsFrontImage,
"shop_price": value.ShopPrice,
"ctegory": map[string]interface{}{
"id": value.Category.Id,
"name": value.Category.Name,
},
"brand": map[string]interface{}{
"id": value.Brand.Id,
"name": value.Brand.Name,
"logo": value.Brand.Logo,
},
"is_hot": value.IsHot,
"is_new": value.IsNew,
"on_sale": value.OnSale,
})
}
reMap["data"] = goodsList
ctx.JSON(http.StatusOK, reMap)
}
1-4如何设计一个符合go风格的注册中心接口
// goods-web.json
{
"consul": {
"host": "127.0.0.1",
"port": 8500
},
"jwt": {
"key": "3hTQH29YPr5oCcwatf1a1KbcnAft8Hn1"
},
"tags": [
"mxshop",
"imooc",
"bobby",
"oss",
"web"
],
"name": "goods-web",
"port": 8022,
"host": "127.0.0.1",
"goods_srv": {
"name": "goods_srv"
}
}
// goods-web/main.go
package main
import (
"fmt"
uuid "github.com/satori/go.uuid"
// ...
)
func main() {
// ...
// 3. 运行接口
// zap.S() 直接拿到zap的suger
// S()和L()有加锁,并且使用的是全局logger,可以全局安全访问
go func() {
zap.S().Infof("启动服务器, 端口: %d", global.ServerConfig.Port)
err := r.Run(fmt.Sprintf(":%d", global.ServerConfig.Port))
if err != nil {
zap.S().Panic("启动失败: ", err.Error())
}
}()
register_cli := consul.NewRegistryClient(
global.ServerConfig.ConsulInfo.Host,
global.ServerConfig.ConsulInfo.Port,
)
serviceId := fmt.Sprintf("%s", uuid.NewV4())
if err := register_cli.Register(
// 要写对外的地址
//"127.0.0.1",
//"192.168.118.137",
global.ServerConfig.Host,
global.ServerConfig.Port,
global.ServerConfig.Name,
global.ServerConfig.Tags,
serviceId,
); err != nil {
zap.S().Panic("服务注册失败: ", err.Error())
}
// 接收终止信号, 注销服务
quit := make(chan os.Signal)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
register_cli.DeRegister(serviceId)
}
// goods-web/config/config.go
package config
// ...
type ServerConfig struct {
Name string `mapstructure:"name" json:"name"`
Host string `mapstructure:"host" json:"host"`
Port int `mapstructure:"port" json:"port"`
Tags []string `mapstructure:"tags" json:"tags"`
GoodsSrvInfo GoodsSrvConfig `mapstructure:"goods_srv" json:"goods_srv"`
JWTInfo JWTConfig `mapstructure:"jwt" json:"jwt"`
ConsulInfo ConsulConfig `mapstructure:"consul" json:"consul"`
}
// goods-web/utils/register/consul/register.go
package consul
import (
"fmt"
"github.com/hashicorp/consul/api"
"go.uber.org/zap"
)
type RegistryClient interface {
Register(addr string, port int, name string, tags []string, id string) error
DeRegister(serviceId string) error
}
type Registry struct {
Host string
Port int
}
func NewRegistryClient(host string, port int) RegistryClient {
return &Registry{
Host: host,
Port: port,
}
}
func (r *Registry) DeRegister(serviceId string) error {
// ...
return nil
}
func (r *Registry) Register(addr string, port int, name string, tags []string, id string) error {
cfg := api.DefaultConfig()
// consul
cfg.Address = fmt.Sprintf("%s:%d", r.Host, r.Port)
client, err := api.NewClient(cfg)
if err != nil {
panic(err)
}
// 生成对应的检查对象
check := &api.AgentServiceCheck{
HTTP: fmt.Sprintf("http://%s:%d/g/v1/base/health", addr, port),
Timeout: "5s",
Interval: "5s",
DeregisterCriticalServiceAfter: "10s",
}
// 生成注册对象
registration := new(api.AgentServiceRegistration)
registration.Name = name
registration.ID = id
registration.Port = port
registration.Tags = tags
registration.Address = addr
registration.Check = check
err = client.Agent().ServiceRegister(registration)
if err != nil {
panic(err)
}
return nil
}
1-5gin的退出后的服务注销
// goods-web/utils/register/consul/register.go
// ...
func (r *Registry) DeRegister(serviceId string) error {
cfg := api.DefaultConfig()
// consul
cfg.Address = fmt.Sprintf("%s:%d", r.Host, r.Port)
cli, err := api.NewClient(cfg)
if err != nil {
panic(err)
}
return cli.Agent().ServiceDeregister(serviceId)
}
1-6用户的web服务服务注册和优雅退出
1-7新建商品
// goods-web/api/goods.go
// ...
// 新增商品
func New(ctx *gin.Context) {
goodsForm := forms.GoodsForm{}
// shouldBind 可以form也可以json
if err := ctx.ShouldBindJSON(&goodsForm); err != nil {
HandleValidatorErr(ctx, err)
return
}
goodsClient := global.GoodsSrvClient
rsp, err := goodsClient.CreateGoods(context.Background(), &proto.CreateGoodsInfo{
Name: goodsForm.Name,
GoodsSn: goodsForm.GoodsSn,
Stocks: goodsForm.Stocks,
MarketPrice: goodsForm.MarketPrice,
ShopPrice: goodsForm.ShopPrice,
GoodsBrief: goodsForm.GoodsBrief,
//GoodsDesc: goodsForm.GoodsDesc,
// binding表单验证 bool类型如果不是指针可能出错
ShipFree: *goodsForm.ShipFree,
Images: goodsForm.Images,
DescImages: goodsForm.DescImages,
GoodsFrontImage: goodsForm.FrontImage,
CategoryId: goodsForm.CategoryId,
Brand: goodsForm.Brand,
})
if err != nil {
HandleGrpcErrorToHttp(err, ctx)
return
}
// todo 如何设置库存
ctx.JSON(http.StatusOK, rsp)
}
// goods-web/forms/goods.go
package forms
type GoodsForm struct {
Name string `form:"name" json:"name" binding:"required,min=2,max=100"`
GoodsSn string `form:"goods_sn" json:"goods_sn" binding:"required,min=2,lt=20"`
Stocks int32 `form:"stocks" json:"stocks" binding:"required,min=1"`
CategoryId int32 `form:"category" json:"category" binding:"required"`
MarketPrice float32 `form:"market_price" json:"market_price" binding:"required,min=0"`
ShopPrice float32 `form:"shop_price" json:"shop_price" binding:"required,min=0"`
GoodsBrief string `form:"goods_brief" json:"goods_brief" binding:"required,min=3"`
Images []string `form:"images" json:"images" binding:"required,min=1"`
DescImages []string `form:"desc_images" json:"desc_images" binding:"required,min=1"`
//GoodsDesc string `form:"desc" json:"desc" binding:"required,min=3"`
ShipFree *bool `form:"ship_free" json:"ship_free" binding:"required"`
FrontImage string `form:"front_image" json:"front_image" binding:"required,url"`
Brand int32 `form:"brand" json:"brand" binding:"required"`
}
// goods-web/router/goods.go
package router
import (
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"mxshop-api/goods-web/api/goods"
)
func InitGoodsRouter(group *gin.RouterGroup) {
gr := group.Group("goods")
zap.S().Info("配置用户相关的url")
{
gr.GET("list", goods.List)
gr.POST("new",middlewares.JWTAuth(),middlewares.IsAdminAuth(),goods.New)
}
}
1-8获取商品详情
// goods-web/api/goods.go
// ...
// 获取商品详情
func Detail(ctx *gin.Context) {
id := ctx.Param("id")
i, err := strconv.ParseInt(id, 10, 32)
if err != nil {
ctx.Status(http.StatusBadRequest)
return
}
r, err := global.GoodsSrvClient.GetGoodsDetail(context.Background(), &proto.GoodInfoRequest{
Id: int32(i),
})
if err != nil {
HandleGrpcErrorToHttp(err, ctx)
return
}
// todo 从库存服务中拿到库存 -> 一般会单独开获取库存的接口,让前端多发一个请求获取
rsp := map[string]interface{}{
"id": r.Id,
"name": r.Name,
"goods_brief": r.GoodsBrief,
"desc": r.GoodsDesc,
"ship_free": r.ShipFree,
"images": r.Images,
"desc_images": r.DescImages,
"front_image": r.GoodsFrontImage,
"shop_price": r.ShopPrice,
"ctegory": map[string]interface{}{
"id": r.Category.Id,
"name": r.Category.Name,
},
"brand": map[string]interface{}{
"id": r.Brand.Id,
"name": r.Brand.Name,
"logo": r.Brand.Logo,
},
"is_hot": r.IsHot,
"is_new": r.IsNew,
"on_sale": r.OnSale,
}
ctx.JSON(http.StatusOK, rsp)
}
// goods-web/router/goods.go
package router
import (
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"mxshop-api/goods-web/api/goods"
"mxshop-api/goods-web/middlewares"
)
func InitGoodsRouter(group *gin.RouterGroup) {
gr := group.Group("goods")
zap.S().Info("配置用户相关的url")
{
gr.GET("", goods.List)
//gr.GET("list", goods.List)
//gr.POST("new",middlewares.JWTAuth(),middlewares.IsAdminAuth(),goods.New)
gr.POST("", middlewares.JWTAuth(), middlewares.IsAdminAuth(), goods.New)
//gr.POST("new", goods.New)
gr.GET("/:id", goods.Detail)
}
}
1-9商品删除,更新
// goods-web/api/goods.go
// ...
// Delete 删除商品
func Delete(ctx *gin.Context) {
id := ctx.Param("id")
i, err := strconv.ParseInt(id, 10, 32)
if err != nil {
ctx.Status(http.StatusBadRequest)
return
}
_, err = global.GoodsSrvClient.DeleteGoods(context.Background(), &proto.DeleteGoodsInfo{Id: int32(i)})
if err != nil {
HandleGrpcErrorToHttp(err, ctx)
return
}
ctx.Status(http.StatusOK)
return
}
// Stocks 获取商品库存
func Stocks(ctx *gin.Context) {
id := ctx.Param("id")
_, err := strconv.ParseInt(id, 10, 32)
if err != nil {
ctx.Status(http.StatusBadRequest)
return
}
// todo 商品库存
return
}
// UpdateStatus 更新商品(部分)
func UpdateStatus(ctx *gin.Context) {
goodsStatusForm := forms.GoodsStatusForm{}
if err := ctx.ShouldBindJSON(&goodsStatusForm); err != nil {
HandleValidatorErr(ctx, err)
return
}
id := ctx.Param("id")
i, err := strconv.ParseInt(id, 10, 32)
if err != nil {
ctx.Status(http.StatusBadRequest)
return
}
_, err = global.GoodsSrvClient.UpdateGoods(context.Background(), &proto.CreateGoodsInfo{
Id: int32(i),
IsNew: *goodsStatusForm.IsNew,
IsHot: *goodsStatusForm.IsHot,
OnSale: *goodsStatusForm.OnSale,
})
if err != nil {
HandleGrpcErrorToHttp(err, ctx)
return
}
ctx.JSON(http.StatusOK, gin.H{
"msg": "修改成功",
})
}
// Update 更新商品
func Update(ctx *gin.Context) {
goodsForm := forms.GoodsForm{}
if err := ctx.ShouldBindJSON(&goodsForm); err != nil {
HandleValidatorErr(ctx, err)
return
}
id := ctx.Param("id")
i, err := strconv.ParseInt(id, 10, 32)
if err != nil {
ctx.Status(http.StatusBadRequest)
return
}
_, err = global.GoodsSrvClient.UpdateGoods(context.Background(), &proto.CreateGoodsInfo{
Id: int32(i),
Name: goodsForm.Name,
GoodsSn: goodsForm.GoodsSn,
Stocks: goodsForm.Stocks,
MarketPrice: goodsForm.MarketPrice,
ShopPrice: goodsForm.ShopPrice,
GoodsBrief: goodsForm.GoodsBrief,
//GoodsDesc: goodsForm.GoodsDesc,
// binding表单验证 bool类型如果不是指针可能出错
ShipFree: *goodsForm.ShipFree,
Images: goodsForm.Images,
DescImages: goodsForm.DescImages,
GoodsFrontImage: goodsForm.FrontImage,
CategoryId: goodsForm.CategoryId,
Brand: goodsForm.Brand,
})
if err != nil {
HandleGrpcErrorToHttp(err, ctx)
return
}
ctx.JSON(http.StatusOK, gin.H{
"msg": "更新成功",
})
}
// goods-web/forms/goods.go
package forms
// ...
type GoodsStatusForm struct {
IsNew *bool `form:"new" json:"new" bingding:"required"`
IsHot *bool `form:"hot" json:"hot" bingding:"required"`
OnSale *bool `form:"sale" json:"sale" bingding:"required"`
}
// goods-web/router/goods.go
package router
import (
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"mxshop-api/goods-web/api/goods"
"mxshop-api/goods-web/middlewares"
)
func InitGoodsRouter(group *gin.RouterGroup) {
gr := group.Group("goods")
zap.S().Info("配置用户相关的url")
{
gr.GET("", goods.List)
//gr.GET("list", goods.List)
//gr.POST("new",middlewares.JWTAuth(),middlewares.IsAdminAuth(),goods.New)
gr.POST("", middlewares.JWTAuth(), middlewares.IsAdminAuth(), goods.New)
//gr.POST("new", goods.New)
gr.GET("/:id", goods.Detail)
gr.DELETE("/:id", middlewares.JWTAuth(), middlewares.IsAdminAuth(), goods.Delete)
gr.GET("/:id/stocks", goods.Stocks)
gr.PATCH("/:id", middlewares.JWTAuth(), middlewares.IsAdminAuth(), goods.Update)
gr.PUT("/:id",middlewares.JWTAuth(), middlewares.IsAdminAuth(), goods.Update)
}
}
1-10商品分类的接口
// goods-web/forms/category.go
package forms
type CategoryForm struct {
Name string `form:"name" json:"name" binding:"required,min=3,max=20"`
ParentCategory int32 `form:"parent" json:"parent"`
Level int32 `form:"level" json:"level" binding:"required,oneof=1 2 3"`
IsTab *bool `form:"is_tab" json:"is_tab" binding:"required"`
}
// 只能修改Name和IsTab是因为和前端展示的页面功能有关
type UpdateCategoryForm struct {
Name string `form:"name" json:"name" binding:"required,min=3,max=20"`
IsTab *bool `form:"is_tab" json:"is_tab"`
}
// goods-web/api/base.go
package api
import (
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"mxshop-api/goods-web/global"
"net/http"
"strings"
)
func RemoveTopStruct(fields map[string]string) map[string]string {
rsp := map[string]string{}
for field, err := range fields {
rsp[field[strings.Index(field, ".")+1:]] = err
}
return rsp
}
// HandleGrpcErrorToHttp grpc的code转换为http状态码
func HandleGrpcErrorToHttp(err error, c *gin.Context) {
if err != nil {
if e, ok := status.FromError(err); ok {
switch e.Code() {
case codes.NotFound:
c.JSON(http.StatusNotFound, gin.H{
"msg": e.Message(),
})
case codes.Internal:
c.JSON(http.StatusInternalServerError, gin.H{
"msg": "内部错误",
})
case codes.InvalidArgument:
c.JSON(http.StatusBadRequest, gin.H{
"msg": "参数错误",
})
default:
c.JSON(http.StatusInternalServerError, gin.H{
//"msg": "其他错误: "+e.Message(),
"msg": "其他错误",
})
}
return
}
}
}
func HandleValidatorErr(c *gin.Context, err error) {
// 如何返回错误信息
errs, ok := err.(validator.ValidationErrors)
if !ok {
c.JSON(http.StatusOK, gin.H{
"msg": err.Error(),
})
}
c.JSON(http.StatusBadRequest, gin.H{
"error": RemoveTopStruct(errs.Translate(global.Trans)),
})
}
// goods-web/initialize/router.go
package initialize
import (
"github.com/gin-gonic/gin"
"mxshop-api/goods-web/middlewares"
"mxshop-api/goods-web/router"
)
func Routers() *gin.Engine {
r := gin.Default()
// 跨域
r.Use(middlewares.Cors())
apiGroup := r.Group("/g/v1")
// 分组注册接口
router.InitGoodsRouter(apiGroup)
router.InitCategoryRouter(apiGroup)
router.InitBaseRouter(apiGroup)
return r
}
// goods-web/router/category.go
package router
import (
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"mxshop-api/goods-web/api/category"
"mxshop-api/goods-web/middlewares"
)
func InitCategoryRouter(group *gin.RouterGroup) {
gr := group.Group("categorys")
zap.S().Info("配置商品分类相关的url")
{
gr.GET("", category.List)
gr.POST("", middlewares.JWTAuth(), middlewares.IsAdminAuth(), category.New)
gr.GET("/:id", category.Detail)
gr.DELETE("/:id", middlewares.JWTAuth(), middlewares.IsAdminAuth(), category.Delete)
gr.PUT("/:id", middlewares.JWTAuth(), middlewares.IsAdminAuth(), category.Update)
}
}
// goods-web/api/category/category.go
package category
import (
"context"
"encoding/json"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"google.golang.org/protobuf/types/known/emptypb"
"mxshop-api/goods-web/api"
"mxshop-api/goods-web/forms"
"mxshop-api/goods-web/global"
"mxshop-api/goods-web/proto"
"net/http"
"strconv"
)
func List(ctx *gin.Context) {
r, err := global.GoodsSrvClient.GetAllCategorysList(context.Background(), &emptypb.Empty{})
if err != nil {
api.HandleGrpcErrorToHttp(err, ctx)
return
}
data := make([]interface{}, 0)
err = json.Unmarshal([]byte(r.JsonData), &data)
if err != nil {
zap.S().Errorw("[List] 查询 [分类列表] 失败: ", err.Error())
}
ctx.JSON(http.StatusOK, data)
}
func Detail(ctx *gin.Context) {
id := ctx.Param("id")
i, err := strconv.ParseInt(id, 10, 32)
if err != nil {
ctx.Status(http.StatusNotFound)
return
}
reMap := make(map[string]interface{})
subCategorys := make([]interface{}, 0)
r, err := global.GoodsSrvClient.GetSubCategory(context.Background(), &proto.CategoryListRequest{
Id: int32(i),
})
if err != nil {
api.HandleGrpcErrorToHttp(err, ctx)
return
}
for _, val := range r.SubCategorys {
subCategorys = append(subCategorys, map[string]interface{}{
"id": val.Id,
"name": val.Name,
"level": val.Level,
"parent_category": val.ParentCategory,
"is_tab": val.IsTab,
})
}
reMap["id"] = r.Info.Id
reMap["name"] = r.Info.Name
reMap["level"] = r.Info.Level
reMap["parent_category"] = r.Info.ParentCategory
reMap["is_tab"] = r.Info.IsTab
reMap["sub_categorys"] = subCategorys
ctx.JSON(http.StatusOK, reMap)
}
func New(ctx *gin.Context) {
categoryForm := forms.CategoryForm{}
if err := ctx.ShouldBindJSON(&categoryForm); err != nil {
api.HandleValidatorErr(ctx, err)
return
}
rsp, err := global.GoodsSrvClient.CreateCategory(context.Background(), &proto.CategoryInfoRequest{
Name: categoryForm.Name,
ParentCategory: categoryForm.ParentCategory,
Level: categoryForm.Level,
IsTab: *categoryForm.IsTab,
})
if err != nil {
api.HandleGrpcErrorToHttp(err, ctx)
return
}
request := make(map[string]interface{})
request["id"] = rsp.Id
request["name"] = rsp.Name
request["parent"] = rsp.ParentCategory
request["level"] = rsp.Level
request["is_tab"] = rsp.IsTab
ctx.JSON(http.StatusOK, request)
}
func Delete(ctx *gin.Context) {
id := ctx.Param("id")
i, err := strconv.ParseInt(id, 10, 32)
if err != nil {
ctx.Status(http.StatusNotFound)
return
}
// 最好不要删除子级
_, err = global.GoodsSrvClient.DeleteCategory(context.Background(), &proto.DeleteCategoryRequest{
Id: int32(i),
})
if err != nil {
api.HandleGrpcErrorToHttp(err, ctx)
return
}
ctx.Status(http.StatusOK)
}
func Update(ctx *gin.Context) {
categoryForm := forms.UpdateCategoryForm{}
if err := ctx.ShouldBindJSON(&categoryForm); err != nil {
api.HandleValidatorErr(ctx, err)
return
}
id := ctx.Param("id")
i, err := strconv.ParseInt(id, 10, 32)
if err != nil {
ctx.Status(http.StatusNotFound)
return
}
request := &proto.CategoryInfoRequest{
Id: int32(i),
Name: categoryForm.Name,
}
if categoryForm.IsTab != nil {
request.IsTab = *categoryForm.IsTab
}
_, err = global.GoodsSrvClient.UpdateCategory(context.Background(), request)
if err != nil {
api.HandleGrpcErrorToHttp(err, ctx)
return
}
ctx.Status(http.StatusOK)
}
1-11轮播图接口和yapi的快速测试
// goods-web/forms/banner.go
package forms
type BannerForm struct {
Image string `form:"image" json:"image" binding:"url"`
Index int `form:"index" json:"index" binding:"required"`
Url string `form:"url" json:"url" binding:"url"`
}
// goods-web/router/banner.go
package router
import (
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"mxshop-api/goods-web/api/banners"
"mxshop-api/goods-web/middlewares"
)
func InitBannerRouter(group *gin.RouterGroup) {
gr := group.Group("banners")
zap.S().Info("配置轮播图相关的url")
{
gr.GET("", banners.List)
gr.POST("", middlewares.JWTAuth(), middlewares.IsAdminAuth(), banners.New)
gr.DELETE("/:id", middlewares.JWTAuth(), middlewares.IsAdminAuth(), banners.Delete)
gr.PUT("/:id", middlewares.JWTAuth(), middlewares.IsAdminAuth(), banners.Update)
}
}
// goods-web/initialize/router.go
package initialize
import (
"github.com/gin-gonic/gin"
"mxshop-api/goods-web/middlewares"
"mxshop-api/goods-web/router"
)
func Routers() *gin.Engine {
r := gin.Default()
// 跨域
r.Use(middlewares.Cors())
apiGroup := r.Group("/g/v1")
// 分组注册接口
router.InitGoodsRouter(apiGroup)
router.InitCategoryRouter(apiGroup)
router.InitBannerRouter(apiGroup)
router.InitBaseRouter(apiGroup)
return r
}
// goods-web/api/banners/banner.go
package banners
import (
"context"
"github.com/gin-gonic/gin"
"google.golang.org/protobuf/types/known/emptypb"
"mxshop-api/goods-web/api"
"mxshop-api/goods-web/forms"
"mxshop-api/goods-web/global"
"mxshop-api/goods-web/proto"
"net/http"
"strconv"
)
func List(ctx *gin.Context) {
rsp, err := global.GoodsSrvClient.BannerList(context.Background(), &emptypb.Empty{})
if err != nil {
api.HandleGrpcErrorToHttp(err, ctx)
return
}
result := make([]interface{}, 0)
for _, value := range rsp.Data {
reMap := make(map[string]interface{})
reMap["id"] = value.Id
reMap["index"] = value.Index
reMap["image"] = value.Image
reMap["ur1"] = value.Url
result = append(result, reMap)
}
ctx.JSON(http.StatusOK, result)
}
func New(ctx *gin.Context) {
form := forms.BannerForm{}
if err := ctx.ShouldBindJSON(&form); err != nil {
api.HandleValidatorErr(ctx, err)
return
}
rsp, err := global.GoodsSrvClient.CreateBanner(context.Background(), &proto.BannerRequest{
Index: int32(form.Index),
Url: form.Url,
Image: form.Image,
})
if err != nil {
api.HandleGrpcErrorToHttp(err, ctx)
return
}
response := make(map[string]interface{})
response["id"] = rsp.Id
response["index"] = rsp.Index
response["url"] = rsp.Url
response["image"] = rsp.Image
ctx.JSON(http.StatusOK, response)
}
func Update(ctx *gin.Context) {
form := forms.BannerForm{}
if err := ctx.ShouldBindJSON(&form); err != nil {
api.HandleValidatorErr(ctx, err)
return
}
id := ctx.Param("id")
i, err := strconv.ParseInt(id, 10, 32)
if err != nil {
ctx.Status(http.StatusNotFound)
return
}
_, err = global.GoodsSrvClient.UpdateBanner(context.Background(), &proto.BannerRequest{
Id: int32(i),
Index: int32(form.Index),
Url: form.Url,
})
if err != nil {
api.HandleGrpcErrorToHttp(err, ctx)
return
}
ctx.Status(http.StatusOK)
}
func Delete(ctx *gin.Context) {
form := forms.BannerForm{}
if err := ctx.ShouldBindJSON(&form); err != nil {
api.HandleValidatorErr(ctx, err)
return
}
id := ctx.Param("id")
i, err := strconv.ParseInt(id, 10, 32)
if err != nil {
ctx.Status(http.StatusNotFound)
return
}
_, err = global.GoodsSrvClient.DeleteBanner(context.Background(), &proto.BannerRequest{
Id: int32(i),
})
if err != nil {
api.HandleGrpcErrorToHttp(err, ctx)
return
}
ctx.Status(http.StatusOK)
}
1-12品牌列表页接口
// goods-web/router/brand.go
package router
import (
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"mxshop-api/goods-web/api/brands"
)
func InitBrandsRouter(group *gin.RouterGroup) {
gr := group.Group("brands")
zap.S().Info("配置品牌相关的url")
{
gr.GET("", brands.BrandList)
gr.DELETE("/:id", brands.DeleteBrand)
gr.POST("", brands.NewBrand)
gr.PUT("/:id", brands.UpdateBrand)
}
cbr := group.Group("categorybrands")
zap.S().Info("配置品牌分类相关的url")
{
cbr.GET("", brands.CategoryBrandList)
//类别品牌
cbr.DELETE("/:id", brands.DeleteCategoryBrand)
cbr.POST("", brands.NewCategoryBrand)
cbr.PUT("/:id", brands.UpdateCategoryBrand)
cbr.GET("/:id", brands.GetCategoryBrandList)
}
}
// goods-web/forms/brand.go
package forms
type BrandForm struct {
Name string `form:"name" json:"name" binding:"required,min=3,max=10"`
Logo string `form:"logo" json:"logo" binding:"url"`
}
type CategoryBrandForm struct {
CategoryId int `form:"category_id" json:"category_id" binding:"required"`
BrandId int `form:"brand_id" json:"brand_id" binding:"required"`
}
// goods-web/initialize/router.go
package initialize
import (
"github.com/gin-gonic/gin"
"mxshop-api/goods-web/middlewares"
"mxshop-api/goods-web/router"
)
func Routers() *gin.Engine {
r := gin.Default()
// 跨域
r.Use(middlewares.Cors())
apiGroup := r.Group("/g/v1")
// 分组注册接口
router.InitGoodsRouter(apiGroup)
router.InitCategoryRouter(apiGroup)
router.InitBannerRouter(apiGroup)
router.InitBrandsRouter(apiGroup)
router.InitBaseRouter(apiGroup)
return r
}
// goods-web/api/brands/brand.go
package brands
import (
"context"
"github.com/gin-gonic/gin"
"mxshop-api/goods-web/api"
"mxshop-api/goods-web/forms"
"mxshop-api/goods-web/global"
"mxshop-api/goods-web/proto"
"net/http"
"strconv"
)
func BrandList(ctx *gin.Context) {
pn := ctx.DefaultQuery("pn", "0")
pnInt, _ := strconv.Atoi(pn)
pSize := ctx.DefaultQuery("psize", "0")
pSizeInt, _ := strconv.Atoi(pSize)
rsp, err := global.GoodsSrvClient.BrandList(context.Background(), &proto.BrandFilterRequest{
Pages: int32(pnInt),
PagePerNums: int32(pSizeInt),
})
if err != nil {
api.HandleGrpcErrorToHttp(err, ctx)
return
}
result := make([]interface{}, 0)
reMap := make(map[string]interface{})
reMap["total"] = rsp.Total
// 手动分页(如果grpc服务没分页)
//for _, val := range rsp.Data[pnInt : pnInt*pSizeInt+pSizeInt] {
for _, val := range rsp.Data {
reMap := make(map[string]interface{})
reMap["id"] = val.Id
reMap["name"] = val.Name
reMap["logo"] = val.Logo
result = append(result, reMap)
}
reMap["data"] = result
ctx.JSON(http.StatusOK, reMap)
}
func NewBrand(ctx *gin.Context) {
form := forms.BrandForm{}
if err := ctx.ShouldBindJSON(&form); err != nil {
api.HandleValidatorErr(ctx, err)
return
}
rsp, err := global.GoodsSrvClient.CreateBrand(context.Background(), &proto.BrandRequest{
Name: form.Name,
Logo: form.Logo,
})
if err != nil {
api.HandleGrpcErrorToHttp(err, ctx)
return
}
res := make(map[string]interface{})
res["id"] = rsp.Id
res["name"] = rsp.Name
res["logo"] = rsp.Logo
ctx.JSON(http.StatusOK, res)
}
func DeleteBrand(ctx *gin.Context) {
id := ctx.Param("id")
i, err := strconv.ParseInt(id, 10, 32)
if err != nil {
ctx.Status(http.StatusNotFound)
return
}
_, err = global.GoodsSrvClient.DeleteBrand(context.Background(), &proto.BrandRequest{Id: int32(i)})
if err != nil {
api.HandleGrpcErrorToHttp(err, ctx)
return
}
ctx.Status(http.StatusOK)
}
func UpdateBrand(ctx *gin.Context) {
form := forms.BrandForm{}
if err := ctx.ShouldBindJSON(&form); err != nil {
api.HandleValidatorErr(ctx, err)
return
}
id := ctx.Param("id")
i, err := strconv.ParseInt(id, 10, 32)
if err != nil {
ctx.Status(http.StatusNotFound)
return
}
_, err = global.GoodsSrvClient.UpdateBrand(context.Background(), &proto.BrandRequest{
Id: int32(i),
Name: form.Name,
Logo: form.Logo,
})
if err != nil {
api.HandleGrpcErrorToHttp(err, ctx)
return
}
ctx.Status(http.StatusOK)
}
// category brand
func GetCategoryBrandList(ctx *gin.Context) {
id := ctx.Param("id")
i, err := strconv.ParseInt(id, 10, 32)
if err != nil {
ctx.Status(http.StatusNotFound)
return
}
rsp, err := global.GoodsSrvClient.GetCategoryBrandList(context.Background(), &proto.CategoryInfoRequest{
Id: int32(i),
})
if err != nil {
api.HandleGrpcErrorToHttp(err, ctx)
return
}
result := make([]interface{}, 0)
for _, val := range rsp.Data {
reMap := make(map[string]interface{})
reMap["id"] = val.Id
reMap["name"] = val.Name
reMap["logo"] = val.Logo
result = append(result, reMap)
}
ctx.JSON(http.StatusOK, result)
}
func CategoryBrandList(ctx *gin.Context) {
rsp, err := global.GoodsSrvClient.CategoryBrandList(context.Background(), &proto.CategoryBrandFilterRequest{})
if err != nil {
api.HandleGrpcErrorToHttp(err, ctx)
return
}
reMap := map[string]interface{}{
"total": rsp.Total,
}
result := make([]interface{}, 0)
for _, val := range rsp.Data {
reMap := make(map[string]interface{})
reMap["id"] = val.Id
reMap["category"] = map[string]interface{}{
"id": val.Category.Id,
"name": val.Category.Name,
}
reMap["brand"] = map[string]interface{}{
"id": val.Brand.Id,
"name": val.Brand.Name,
"logo": val.Brand.Logo,
}
result = append(result, reMap)
}
reMap["data"] = result
ctx.JSON(http.StatusOK, reMap)
}
func NewCategoryBrand(ctx *gin.Context) {
categoryBrandForm := forms.CategoryBrandForm{}
if err := ctx.ShouldBindJSON(&categoryBrandForm); err != nil {
api.HandleValidatorErr(ctx, err)
return
}
rsp, err := global.GoodsSrvClient.CreateCategoryBrand(context.Background(), &proto.CategoryBrandRequest{
CategoryId: int32(categoryBrandForm.CategoryId),
BrandId: int32(categoryBrandForm.BrandId),
})
if err != nil {
api.HandleGrpcErrorToHttp(err, ctx)
return
}
response := make(map[string]interface{})
response["id"] = rsp.Id
ctx.JSON(http.StatusOK, response)
}
func UpdateCategoryBrand(ctx *gin.Context) {
categoryBrandForm := forms.CategoryBrandForm{}
if err := ctx.ShouldBindJSON(&categoryBrandForm); err != nil {
api.HandleValidatorErr(ctx, err)
return
}
id := ctx.Param("id")
i, err := strconv.ParseInt(id, 10, 32)
if err != nil {
api.HandleGrpcErrorToHttp(err, ctx)
return
}
_, err = global.GoodsSrvClient.UpdateCategoryBrand(context.Background(), &proto.CategoryBrandRequest{
Id: int32(i),
CategoryId: int32(categoryBrandForm.CategoryId),
BrandId: int32(categoryBrandForm.BrandId),
})
if err != nil {
api.HandleGrpcErrorToHttp(err, ctx)
return
}
ctx.Status(http.StatusOK)
}
func DeleteCategoryBrand(ctx *gin.Context) {
id := ctx.Param("id")
i, err := strconv.ParseInt(id, 10, 32)
if err != nil {
ctx.Status(http.StatusNotFound)
return
}
_, err = global.GoodsSrvClient.DeleteCategoryBrand(context.Background(), &proto.CategoryBrandRequest{
Id: int32(i),
})
if err != nil {
api.HandleGrpcErrorToHttp(err, ctx)
return
}
ctx.Status(http.StatusOK)
}
第2章阿里云的oss服务集成
2-1为什么要使用阿里云oss
2-2oss的基本概念介绍
阿里云太贵了, 我用minio来搞
// goods-web/test/minio-oss/main.go
package main
import (
"context"
"fmt"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
"log"
"net/url"
"time"
)
const (
endpoint string = "localhost:9005"
accessKeyID string = "minioadmin"
secretAccessKey string = "minioadmin"
//accessKeyID string = "P8Y0aw9ssfXy4y20VuQx"
//secretAccessKey string = "JPhpdOy1cjVJs2RPUSTrQxjPLDKjlvwb9fkECdkx"
useSSL bool = false
)
var (
client *minio.Client
//err error
)
// Init 创建连接
func Init() error {
var err error
client, err = minio.New(endpoint, &minio.Options{
Creds: credentials.NewStaticV4(accessKeyID, secretAccessKey, ""),
Secure: useSSL,
})
if err != nil {
log.Fatalln("minio连接错误: ", err)
}
log.Printf("%#v\n", client)
return err
}
// CreateBucket 创建存储桶bucket
func CreateBucket(ctx context.Context, bucketName, region string) error {
//bucketName := "mymusic"
var err error
err = client.MakeBucket(ctx, bucketName, minio.MakeBucketOptions{
// Region bucket的地域位置
//Region: "cn-south-1",
Region: region,
ObjectLocking: false,
})
if err != nil {
log.Println("创建bucket错误: ", err)
exists, _ := client.BucketExists(ctx, bucketName)
if exists {
log.Printf("bucket: %s已经存在", bucketName)
}
} else {
log.Printf("Successfully created %s\n", bucketName)
}
return err
}
// ListBucket 获取所有bucket
func ListBucket(ctx context.Context) error {
buckets, err := client.ListBuckets(ctx)
for _, bucket := range buckets {
fmt.Println(bucket)
}
return err
}
// FileUploader 文件上传
func FileUploader(
ctx context.Context,
bucketName string,
objectName string, // 在桶中的文件名
filePath string,
contextType string,
) (string, error) {
//bucketName := "mymusic"
//objectName := "audit.log"
//filePath := "./audit.log"
//contextType := "application/text"
object, err := client.FPutObject(
ctx,
bucketName,
objectName, filePath,
minio.PutObjectOptions{
ContentType: contextType,
},
)
if err != nil {
log.Println("上传失败:", err)
}
presignedURL, err := GeneratePresignedURL(ctx, bucketName, objectName)
if err != nil {
return "", err
}
log.Printf("Successfully uploaded %s of size %d\n", objectName, object.Size)
fmt.Println("Successfully generated presigned URL", presignedURL)
return presignedURL, err
}
// GeneratePresignedURL 生成预签名url
func GeneratePresignedURL(
ctx context.Context,
bucketName, objectName string,
) (string, error) {
// 获取url
// Set request parameters for content-disposition.
reqParams := make(url.Values)
reqParams.Set("response-content-disposition", fmt.Sprintf("attachment; filename=\"%s\"", objectName))
// Generates a presigned url which expires in a day.
presignedURL, err := client.PresignedGetObject(
ctx, bucketName, objectName,
// 7天有效期(minio最大值)
time.Second*604800, reqParams,
)
if err != nil {
fmt.Println(err)
return "", err
}
return presignedURL.String(), nil
}
// FileGet 下载文件
func FileGet(
ctx context.Context,
bucketName string,
objectName string, // 存在桶中的文件名
filePath string, // 存到哪里
) error {
//bucketName := "mymusic"
//objectName := "audit.log"
//filePath := "./audit2.log"
var err error
err = client.FGetObject(ctx, bucketName, objectName, filePath, minio.GetObjectOptions{})
if err != nil {
log.Println("下载错误: ", err)
}
return err
}
// FilesDelete 从桶中删除文件
func FilesDelete(ctx context.Context, bucketName, objectName string) error {
//bucketName := "mymusic"
//objectName := "audit.log"
//删除一个文件
var err error
err = client.RemoveObject(ctx, bucketName, objectName, minio.RemoveObjectOptions{GovernanceBypass: true})
//批量删除文件
objectsCh := make(chan minio.ObjectInfo)
go func() {
defer close(objectsCh)
options := minio.ListObjectsOptions{Prefix: "test", Recursive: true}
for object := range client.ListObjects(ctx, bucketName, options) {
if object.Err != nil {
log.Println(object.Err)
}
objectsCh <- object
}
}()
client.RemoveObjects(ctx, objectName, objectsCh, minio.RemoveObjectsOptions{})
return err
}
func main() {
err := Init()
if err != nil {
return
}
err = CreateBucket(context.Background(), "test-1", "cn-south-1")
if err != nil {
return
}
err = ListBucket(context.Background())
if err != nil {
return
}
_, err = FileUploader(
context.Background(),
"test-1",
"test.jpg",
"D:\\go\\projs\\imooc\\mxshop-api\\goods-web\\test\\static\\1.jpg",
"application/octet-stream",
)
if err != nil {
return
}
err = FileGet(context.Background(),
"test-1", "test.jpg",
"./goods-web/test/static/test.jpg")
if err != nil {
return
}
err = FilesDelete(context.Background(), "test-1", "test.jpg")
if err != nil {
return
}
}
2-4前端直传oss的流程
2-5gin集成前端直传文件
2-6为什么我们需要内网穿透
…
第13周 库存服务和分布式锁
第1章库存服务
1-1库存服务的重要性
1-2表结构设计
// inventory_srv/model/inventory.go
package model
// 商品可以放多个仓库, 仓库可以放多个商品
// 这里就用最简单的情况, 仓库里有库存
//type Stock struct {
// BaseModel
// Name string
// Address string
//}
// Inventory
type Inventory struct {
BaseModel
Goods int32 `gorm:"type:int;index"` // 关键字段加索引
// Stocks 库存
Stocks int32 `gorm:"type:int"`
//Stock Stock
// 用于实现分布式乐观锁
Version int32 `gorm:"type:int"`
}
// inventory_srv/model/main/main.go
package main
import (
_ "github.com/anaskhan96/go-password-encoder"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"gorm.io/gorm/schema"
"log"
"mxshop_srvs/inventory_srv/model"
"os"
"time"
)
func main() {
dsn := "root:123456@tcp(127.0.0.1:3307)/mxshop_inventory_srv?charset=utf8mb4&parseTime=True&loc=Local"
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
logger.Config{
SlowThreshold: time.Second, // 慢 SQL 阈值
LogLevel: logger.Info,
Colorful: true, // 禁用彩色打印
},
)
// 全局模式
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
NamingStrategy: schema.NamingStrategy{
// 为false会根据 结构体名+s 作为表名
SingularTable: true,
},
Logger: newLogger,
})
if err != nil {
panic(err)
}
// 定义一个表结构, 生成对应表
_ = db.AutoMigrate(
&model.Inventory{},
)
}
1-3proto接口设计
// inventory_srv/proto/inventory.proto
syntax = "proto3";
import "google/protobuf/empty.proto";
option go_package = ".;proto";
service Inventory {
// 设置库存
rpc SetInv(GoodsInvInfo) returns(google.protobuf.Empty);
// 获取库存信息
rpc InvDetail(GoodsInvInfo) returns(GoodsInvInfo);
// 销售(库存扣减) 一般买东西是从购物车里买, 会有多个商品, 有事务问题(批量成功or失败)
rpc Sell(SellInfo) returns(google.protobuf.Empty);
// 库存归还 -> 分布式事务中重点
rpc Reback(SellInfo) returns(google.protobuf.Empty);
}
message GoodsInvInfo {
int32 goodsId = 1;
int32 num = 2;
}
message SellInfo {
repeated GoodsInvInfo goodsInfo = 1;
}
1-4快速启动库存服务
// inventory_srv.json
{
"name": "inventory_srv",
"host": "127.0.0.1",
"tags": [
"imooc",
"inventory",
"srv"
],
"mysql": {
"host": "127.0.0.1",
"port": 3307,
"user": "root",
"password": "123456",
"db": "mxshop_inventory_srv"
},
"consul": {
"host": "127.0.0.1",
"port": 8500
}
}
// inventory_srv/main.go
package main
import (
"flag"
"fmt"
"github.com/satori/go.uuid"
"go.uber.org/zap"
"google.golang.org/grpc"
"google.golang.org/grpc/health"
"google.golang.org/grpc/health/grpc_health_v1"
"mxshop_srvs/inventory_srv/global"
"mxshop_srvs/inventory_srv/initialize"
"mxshop_srvs/inventory_srv/proto"
"mxshop_srvs/inventory_srv/utils"
"mxshop_srvs/inventory_srv/utils/register/consul"
"net"
"os"
"os/signal"
"syscall"
)
func main() {
// 让用户从命令行传递
//IP := flag.String("ip", "0.0.0.0", "ip地址")
IP := flag.String("ip", "0.0.0.0", "ip地址")
//Port := flag.Int("port", 50051, "端口号") // 用于测试API
Port := flag.Int("port", 0, "端口号")
// 初始化日志
initialize.InitLogger()
initialize.InitConfig()
initialize.InitDB()
zap.S().Info(global.ServerConfig)
flag.Parse()
zap.S().Info("ip: ", *IP)
if *Port == 0 {
*Port, _ = utils.GetFreePort()
}
zap.S().Info("port: ", *Port)
server := grpc.NewServer()
proto.RegisterInventoryServer(server, &proto.UnimplementedInventoryServer{})
lis, err := net.Listen("tcp", fmt.Sprintf("%s:%d", *IP, *Port))
if err != nil {
panic("failed to listen: " + err.Error())
}
// 注册服务健康检查
grpc_health_v1.RegisterHealthServer(server, health.NewServer())
register_cli := consul.NewRegistryClient(
global.ServerConfig.ConsulInfo.Host,
global.ServerConfig.ConsulInfo.Port,
)
serviceId := fmt.Sprintf("%s", uuid.NewV4())
if err := register_cli.Register(
global.ServerConfig.Host,
//global.ServerConfig.Port,
*Port,
global.ServerConfig.Name,
global.ServerConfig.Tags,
serviceId,
); err != nil {
zap.S().Panic("服务注册失败: ", err.Error())
}
// 启动服务
go func() {
// 这里是阻塞代码, 后面的执行不到, 所以改造为异步
err = server.Serve(lis)
if err != nil {
panic("failed to start grpc: " + err.Error())
}
}()
// 接收终止信号, 注销服务
quit := make(chan os.Signal)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
if err := register_cli.DeRegister(serviceId); err != nil {
zap.S().Info("注销失败")
panic(err)
}
zap.S().Info("注销成功")
}
// inventory_srv/utils/register/consul/register.go
package consul
import (
"fmt"
"github.com/hashicorp/consul/api"
)
type RegistryClient interface {
Register(addr string, port int, name string, tags []string, id string) error
DeRegister(serviceId string) error
}
type Registry struct {
Host string
Port int
}
func NewRegistryClient(host string, port int) RegistryClient {
return &Registry{
Host: host,
Port: port,
}
}
func (r *Registry) DeRegister(serviceId string) error {
cfg := api.DefaultConfig()
// consul
cfg.Address = fmt.Sprintf("%s:%d", r.Host, r.Port)
cli, err := api.NewClient(cfg)
if err != nil {
panic(err)
}
return cli.Agent().ServiceDeregister(serviceId)
}
func (r *Registry) Register(addr string, port int, name string, tags []string, id string) error {
cfg := api.DefaultConfig()
// consul
cfg.Address = fmt.Sprintf("%s:%d", r.Host, r.Port)
client, err := api.NewClient(cfg)
if err != nil {
panic(err)
}
// 生成对应的检查对象
check := &api.AgentServiceCheck{
// 0.0.0.0可以用于监听,但是不能用来访问(比如consul进行健康检测)
GRPC: fmt.Sprintf("%s:%d", addr, port),
Timeout: "5s",
Interval: "5s",
DeregisterCriticalServiceAfter: "15s",
}
// 生成注册对象
registration := new(api.AgentServiceRegistration)
registration.Name = name
registration.ID = id
registration.Port = port
registration.Tags = tags
registration.Address = addr
registration.Check = check
err = client.Agent().ServiceRegister(registration)
if err != nil {
panic(err)
}
return nil
}
// inventory_srv/global/global.go
package global
import (
"gorm.io/gorm"
"mxshop_srvs/inventory_srv/config"
)
var (
NacosConfig *config.NacosConfig = &config.NacosConfig{}
DB *gorm.DB
ServerConfig *config.ServerConfig = &config.ServerConfig{}
)
host: 127.0.0.1
port: 8848
namespace: f6fddbfe-80ee-4531-806e-d169af325e23
user: nacos
password: nacos
dataid: inventory_srv.json
group: dev
1-5设置库存和获取库存接口
// inventory_srv/handler/inventory.go
package handler
import (
"context"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
"mxshop_srvs/inventory_srv/global"
"mxshop_srvs/inventory_srv/model"
"mxshop_srvs/inventory_srv/proto"
)
type InventoryServer struct {
proto.UnimplementedInventoryServer
}
// SetInv 设置库存(更新?如何)
func (i *InventoryServer) SetInv(ctx context.Context, in *proto.GoodsInvInfo) (*emptypb.Empty, error) {
var inv model.Inventory
global.DB.Where(&model.Inventory{Goods: in.GoodsId}).First(&inv)
//if inv.Goods == 0 {
// 没查到, goodid为默认值0
inv.Goods = in.GoodsId
//}
inv.Stocks = in.Num
global.DB.Save(&inv)
return &emptypb.Empty{}, nil
}
// InvDetail 获取详情
func (i *InventoryServer) InvDetail(ctx context.Context, in *proto.GoodsInvInfo) (*proto.GoodsInvInfo, error) {
var inv model.Inventory
if result := global.DB.Where(&model.Inventory{Goods: in.GoodsId}).First(&inv); result.RowsAffected == 0 {
return nil, status.Errorf(codes.NotFound, "库存信息不存在")
}
return &proto.GoodsInvInfo{
GoodsId: inv.Goods,
Num: inv.Stocks,
}, nil
}
1-6本地数据库事务解决库存扣减的失败问题
// inventory_srv/handler/inventory.go
package handler
// ...
// Sell 扣减库存
func (i *InventoryServer) Sell(ctx context.Context, in *proto.SellInfo) (*emptypb.Empty, error) {
// 整个订单的库存扣除应该是一整个事务, 要么都成功, 要么都失败
// 并发之下可能出现超卖
tx := global.DB.Begin() // 手动事务(本地)
for _, goodInfo := range in.GoodsInfo {
var inv model.Inventory
if result := global.DB.Where(&model.Inventory{Goods: goodInfo.GoodsId}).First(&inv); result.RowsAffected == 0 {
tx.Rollback() // 回滚之前的操作
return nil, status.Errorf(codes.InvalidArgument, "库存信息不存在")
}
// 库存不够
if inv.Stocks < goodInfo.Num {
tx.Rollback() // 回滚之前的操作
return nil, status.Errorf(codes.ResourceExhausted, "库存不足")
}
// 扣减 可能出现数据不一致 -> 锁 分布式锁
inv.Stocks -= goodInfo.Num
tx.Save(&inv)
}
tx.Commit() // 手动提交
return &emptypb.Empty{}, nil
}
1-7订单超时归还的重要性
// inventory_srv/handler/inventory.go
// ...
// Reback 库存归还 - 订单超时归还|订单创建失败|手动归还
func (i *InventoryServer) Reback(ctx context.Context, in *proto.SellInfo) (*emptypb.Empty, error) {
tx := global.DB.Begin() // 手动事务(本地)
for _, goodInfo := range in.GoodsInfo {
var inv model.Inventory
if result := global.DB.Where(&model.Inventory{Goods: goodInfo.GoodsId}).First(&inv); result.RowsAffected == 0 {
tx.Rollback() // 回滚之前的操作
return nil, status.Errorf(codes.InvalidArgument, "库存信息不存在")
}
// 扣减 可能出现数据不一致 -> 锁 分布式锁
inv.Stocks += goodInfo.Num
tx.Save(&inv)
}
tx.Commit() // 手动提交
return &emptypb.Empty{}, nil
}
1-8测试库存接口
// inventory_srv/test/inventory/main.go
package main
import (
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"mxshop_srvs/inventory_srv/proto"
)
var cli proto.InventoryClient
var conn *grpc.ClientConn
func Init() {
var err error
conn, err = grpc.Dial(
fmt.Sprintf("127.0.0.1:50460"),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
panic(err)
}
cli = proto.NewInventoryClient(conn)
}
func TestSetInv(goodsId, Num int32) {
_, err := cli.SetInv(context.Background(), &proto.GoodsInvInfo{
GoodsId: goodsId, Num: Num,
})
if err != nil {
panic(err)
}
fmt.Println("设置成功")
}
func TestInvDetail(goodsId int32) {
rsp, err := cli.InvDetail(context.Background(), &proto.GoodsInvInfo{
GoodsId: goodsId,
})
if err != nil {
panic(err)
}
fmt.Println(rsp)
}
func TestSell() {
_, err := cli.Sell(context.Background(), &proto.SellInfo{
GoodsInfo: []*proto.GoodsInvInfo{
{GoodsId: 421, Num: 10},
//{GoodsId: 422, Num: 50},
},
})
if err != nil {
panic(err)
}
fmt.Println("库存扣减成功")
}
func TestReback() {
_, err := cli.Reback(context.Background(), &proto.SellInfo{
GoodsInfo: []*proto.GoodsInvInfo{
{GoodsId: 421, Num: 10},
//{GoodsId: 422, Num: 50},
},
})
if err != nil {
panic(err)
}
fmt.Println("库存归还成功")
}
func main() {
Init()
TestSetInv(421, 90)
TestInvDetail(421)
TestSell()
TestReback()
conn.Close()
}
1-9为所有的商品添加库存信息
func main() {
Init()
var i int32
for i = 421; i < 840; i++ {
TestSetInv(i, 100)
}
conn.Close()
}
第2章分布式锁
2-1并发场景下的库存扣减不正确的问题
func TestSell(wg *sync.WaitGroup) {
defer wg.Done()
_, err := cli.Sell(context.Background(), &proto.SellInfo{
GoodsInfo: []*proto.GoodsInvInfo{
//{GoodsId: 421, Num: 10},
{GoodsId: 421, Num: 1},
//{GoodsId: 422, Num: 50},
},
})
if err != nil {
panic(err)
}
fmt.Println("库存扣减成功")
}
func main() {
Init()
// 10个协程并发扣库存
var wg sync.WaitGroup
wg.Add(10)
for i := 0; i < 10; i++ {
go TestSell(&wg)
}
wg.Wait()
conn.Close()
}
2-2通过锁解决并发的问题
// inventory_srv/handler/inventory.go
// 全局互斥锁
var m sync.Mutex
// ...
// Sell 扣减库存
func (i *InventoryServer) Sell(ctx context.Context, in *proto.SellInfo) (*emptypb.Empty, error) {
// 整个订单的库存扣除应该是一整个事务, 要么都成功, 要么都失败
// 并发之下可能出现超卖
tx := global.DB.Begin() // 手动事务(本地)
m.Lock() // 加锁
for _, goodInfo := range in.GoodsInfo {
var inv model.Inventory
if result := global.DB.Where(&model.Inventory{Goods: goodInfo.GoodsId}).First(&inv); result.RowsAffected == 0 {
tx.Rollback() // 回滚之前的操作
return nil, status.Errorf(codes.InvalidArgument, "库存信息不存在")
}
// 库存不够
if inv.Stocks < goodInfo.Num {
tx.Rollback() // 回滚之前的操作
return nil, status.Errorf(codes.ResourceExhausted, "库存不足")
}
// 扣减 可能出现数据不一致 -> 锁 分布式锁
inv.Stocks -= goodInfo.Num
tx.Save(&inv)
}
tx.Commit() // 手动提交
m.Unlock() // (真正操作完数据库时)解锁
return &emptypb.Empty{}, nil
}
2-5mysql的forupdate语句实现悲观锁
2-6gorm实现forupdate悲观锁
// inventory_srv/handler/inventory.go
// Sell 扣减库存
func (i *InventoryServer) Sell(ctx context.Context, in *proto.SellInfo) (*emptypb.Empty, error) {
// 整个订单的库存扣除应该是一整个事务, 要么都成功, 要么都失败
// 并发之下可能出现超卖
tx := global.DB.Begin() // 手动事务(本地)
//m.Lock() // 加锁
for _, goodInfo := range in.GoodsInfo {
var inv model.Inventory
//if result := global.DB.Where(&model.Inventory{Goods: goodInfo.GoodsId}).First(&inv); result.RowsAffected == 0 {
if result :=
// gorm实现mysql的forupdate悲观锁
tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Where(&model.Inventory{Goods: goodInfo.GoodsId}).
First(&inv); result.RowsAffected == 0 {
tx.Rollback() // 回滚之前的操作
return nil, status.Errorf(codes.InvalidArgument, "库存信息不存在")
}
// 库存不够
if inv.Stocks < goodInfo.Num {
tx.Rollback() // 回滚之前的操作
return nil, status.Errorf(codes.ResourceExhausted, "库存不足")
}
// 扣减 可能出现数据不一致 -> 锁 分布式锁
inv.Stocks -= goodInfo.Num
tx.Save(&inv)
}
tx.Commit() // 手动提交
//m.Unlock() // (真正操作完数据库时)解锁
return &emptypb.Empty{}, nil
}
2-7基于mysql的乐观锁实现原理
2-8gorm实现基于mysql的乐观锁
func (i *InventoryServer) Sell(ctx context.Context, in *proto.SellInfo) (*emptypb.Empty, error) {
// 整个订单的库存扣除应该是一整个事务, 要么都成功, 要么都失败
// 并发之下可能出现超卖
tx := global.DB.Begin() // 手动事务(本地)
//m.Lock() // 加锁
for _, goodInfo := range in.GoodsInfo {
var inv model.Inventory
//if result := global.DB.Where(&model.Inventory{Goods: goodInfo.GoodsId}).First(&inv); result.RowsAffected == 0 {
// 扣减失败循环重试
for {
if result :=
// gorm实现mysql的forupdate悲观锁
tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Where(&model.Inventory{Goods: goodInfo.GoodsId}).
First(&inv); result.RowsAffected == 0 {
tx.Rollback() // 回滚之前的操作
return nil, status.Errorf(codes.InvalidArgument, "库存信息不存在")
}
// 库存不够
if inv.Stocks < goodInfo.Num {
tx.Rollback() // 回滚之前的操作
return nil, status.Errorf(codes.ResourceExhausted, "库存不足")
}
// 扣减 可能出现数据不一致 -> 锁 分布式锁
inv.Stocks -= goodInfo.Num
// update inventory set stocks stocks-1,version=version+1 where goods=goods and version=version
if result := tx.Model(&model.Inventory{}).
Where("goods=? and version=?", goodInfo.GoodsId, inv.Version).
// 强制字段零值也能更新
Select("Stocks", "Version").
//这种写法有瑕疵,为什么? -> 案例: 剩1个, 10个人抢, 全都成功, 库存却还是为1, -> set 0 被gorm忽略
//零值对于int类型来说默认值是0 (update stocks: 0) 这种会被gorm给忽略掉
Updates(model.Inventory{Stocks: inv.Stocks, Version: inv.Version + 1}); result.RowsAffected == 0 {
zap.S().Info("库存扣减失败")
} else {
break
}
//tx.Save(&inv)
}
}
tx.Commit() // 手动提交
//m.Unlock() // (真正操作完数据库时)解锁
return &emptypb.Empty{}, nil
}
2-9基于redsync的分布式锁实现同步
package main
import (
"fmt"
goredislib "github.com/go-redis/redis/v8"
"github.com/go-redsync/redsync/v4"
"github.com/go-redsync/redsync/v4/redis/goredis/v8"
"sync"
"time"
)
func main() {
client := goredislib.NewClient(&goredislib.Options{
Addr: "localhost:6379",
})
pool := goredis.NewPool(client)
rs := redsync.New(pool)
gNum := 2
// 锁
mutexname := "421"
var wg sync.WaitGroup
wg.Add(gNum)
for i := 0; i < gNum; i++ {
go func() {
defer wg.Done()
mutex := rs.NewMutex(mutexname)
fmt.Println("开始获取锁")
if err := mutex.Lock(); err != nil {
panic(err)
}
fmt.Println("获取锁成功")
// do work
time.Sleep(time.Second * 5)
fmt.Println("开始释放锁")
if ok, err := mutex.Unlock(); !ok || err != nil {
panic("Unlock failed")
}
fmt.Println("释放锁成功")
}()
}
wg.Wait()
}
2-10redsync集成到库存服务中
func (i *InventoryServer) Sell(ctx context.Context, in *proto.SellInfo) (*emptypb.Empty, error) {
//client := goredislib.NewClient(&goredislib.Options{
// Addr: "localhost:6379",
//})
//pool := goredis.NewPool(client)
//rs := redsync.New(pool)
// 整个订单的库存扣除应该是一整个事务, 要么都成功, 要么都失败
// 并发之下可能出现超卖
tx := global.DB.Begin() // 手动事务(本地)
//m.Lock() // 加锁
for _, goodInfo := range in.GoodsInfo {
var inv model.Inventory
//if result := global.DB.Where(&model.Inventory{Goods: goodInfo.GoodsId}).First(&inv); result.RowsAffected == 0 {
//for {
// ...
// 扣减 可能出现数据不一致 -> 锁 分布式锁
inv.Stocks -= goodInfo.Num
tx.Save(&inv)
if ok, err := mutex.Unlock(); !ok || err != nil {
return nil, status.Errorf(codes.Internal, "释放redis分布式锁异常")
}
}
tx.Commit() // 手动提交
//m.Unlock() // (真正操作完数据库时)解锁
return &emptypb.Empty{}, nil
}
2-11redis分布式锁源码解析-setnx的作用
2-12redis分布式锁源码解析-过期时间和延长锁过期时间机制
2-13redis分布式锁源码解析-如何防止锁被其他的gorou
2-14redis的分布式锁在集群环境之下容易出现的问题
2-15redlock源码分析
第14周 和购物车微服务
第1章订单和购物车服务-service
1-1需求分析
1-2订单相关表结构设计
// order_srv/model/main/main.go
package main
import (
_ "github.com/anaskhan96/go-password-encoder"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"gorm.io/gorm/schema"
"log"
"mxshop_srvs/order_srv/model"
"os"
"time"
)
func main() {
dsn := "root:123456@tcp(127.0.0.1:3307)/mxshop_order_srv?charset=utf8mb4&parseTime=True&loc=Local"
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
logger.Config{
SlowThreshold: time.Second, // 慢 SQL 阈值
LogLevel: logger.Info,
Colorful: true, // 禁用彩色打印
},
)
// 全局模式
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
NamingStrategy: schema.NamingStrategy{
// 为false会根据 结构体名+s 作为表名
SingularTable: true,
},
Logger: newLogger,
})
if err != nil {
panic(err)
}
// 定义一个表结构, 生成对应表
_ = db.AutoMigrate(
&model.ShoppingCart{},
&model.OrderInfo{},
&model.OrderGoods{},
)
}