体系课-Go开发工程师2023全新版

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中同步服务信息并进行负载均衡

这个库import后执行初始化, 注册解析器

// 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的安装


nacos
nacos报错

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生成表结构和导入数据


导入数据的sql地址

mxshop_goods_srv.sql

// 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接口

goods.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的流程

前端如果用sdk传, 则需要存放和传输密钥, 很不安全解决方案
如果传输后需要一些业务逻辑,可以使用这种带回调的直传

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分布式锁源码解析-过期时间和延长锁过期时间机制

设置了过期时间, 防止死锁
使用lua脚本判断是否延长过期时间(lua脚本代表一个原子操作)

2-13redis分布式锁源码解析-如何防止锁被其他的gorou


lock时生成随机值

2-14redis的分布式锁在集群环境之下容易出现的问题

master宕机, 新选的master来不及同步and加锁(或者前一个master接收加锁请求时宕机), 后一个服务就会拿到锁

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{},
    )
} 

1-3proto接口定义

07-阶段七:效率工具开发


  目录