从零开始Go微服务电商系统开发 第一章用户服务

1. 定义用户表结构

package model

import "time"
import "gorm.io/gorm"

// 基础模型结构体,包含通用字段
type BaseModel struct {
	ID        int32          `gorm:"primary_key;AUTO_INCREMENT"` // 主键,自动递增
	CreatedAt time.Time      `gorm:"column:add_time"`            // 创建时间
	UpdatedAt time.Time      `gorm:"column:update_time"`         // 更新时间
	DeletedAt gorm.DeletedAt // 软删除时间,GORM 内置类型
	IsDeleted bool           // 是否删除
}

// 用户模型结构体,嵌入基础模型结构体
type User struct {
	BaseModel
	Mobile   string     `gorm:"index:idx_mobile;unique;type:varchar(11);not null"`                      // 手机号,唯一索引,长度11字符,不能为空
	Password string     `gorm:"type:varchar(100);not null"`                                             // 密码,长度100字符,不能为空
	Nickname string     `gorm:"type:varchar(20)"`                                                       // 昵称,长度20字符
	Birthday *time.Time `gorm:"type:datetime"`                                                          // 生日,日期时间类型,使用指针以便区分空值
	Gender   string     `gorm:"column:gender;default:male;type:varchar(6) comment 'female表示女,male表示男'"` // 性别,默认值为'male',长度6字符,注释'female表示女,male表示男'
	Role     int        `gorm:"column:role;default:1;not null;type:int comment '1表示普通用户,2表示管理员'"`       // 角色,默认值为1,不能为空,注释'1表示普通用户,2表示管理员'
}

2.同步表结构

package main

import (
	"gorm.io/driver/mysql"        // 导入 MySQL 驱动
	"gorm.io/gorm"                // 导入 GORM 库
	"gorm.io/gorm/logger"         // 导入 GORM 的日志模块
	"gorm.io/gorm/schema"         // 导入 GORM 的模式定义模块
	"log"                         // 导入日志模块
	"mxshop_server/usr_srv/model" // 导入模型包
	"os"                          // 导入操作系统功能
	"time"                        // 导入时间模块
)

func main() {
	// 数据库连接信息
	dsn := "root:password@tcp(119.23.xxx.xxx:3306)/mxshop_user_srv?charset=utf8mb4&parseTime=True&loc=Local"

	// 配置新的日志记录器
	newLogger := logger.New(
		log.New(os.Stdout, "\r\n", log.LstdFlags), // 使用标准输出作为日志输出
		logger.Config{
			SlowThreshold: time.Second, // 设置慢查询阈值为 1 秒
			LogLevel:      logger.Info, // 设置日志级别为 Info
			Colorful:      true,        // 开启彩色输出
		})

	// 连接数据库
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
		NamingStrategy: schema.NamingStrategy{
			SingularTable: true, // 使用单数表名
		},
		Logger: newLogger, // 使用新的日志记录器
	})
	if err != nil {
		panic(err)
	}

	// 自动迁移模型
	_ = db.AutoMigrate(&model.User{})
}

3. 使用md5盐值加密密码

	// 配置密码选项
	options := &password.Options{16, 100, 32, sha512.New}
	// 生成加密密码
	salt, encodePwd := password.Encode("generic password", options)
	newPassword := fmt.Sprintf("$pbkdf2-sha512$%s$%s", salt, encodePwd)
	fmt.Println(len(newPassword))
	fmt.Println(newPassword)

	// 拆分加密密码信息
	passwordInfo := strings.Split(newPassword, "$")
	fmt.Println(passwordInfo)
	// 验证密码
	check := password.Verify("generic password", passwordInfo[2], passwordInfo[3], options)
	fmt.Println(check)

4. proto结构表定义

syntax = "proto3";  // 使用 proto3 语法

option go_package = ".;proto";  // 指定 Go 语言生成的包路径为当前路径下的 proto 包

import "google/protobuf/empty.proto"; // 导入 Google 的 empty.proto 文件

// 定义 User 服务
service User {
  rpc GetUserList(PageInfo) returns (UserListResponse);  // 获取用户列表
  rpc GetUserByMobile(MobileRequest) returns (UserInfoResponse);  // 通过手机号查询用户
  rpc GetUserById(IdRequest) returns (UserInfoResponse); // 通过用户ID查询用户
  rpc CreateUser(CreateUserInfo) returns (UserInfoResponse); // 添加用户
  rpc UpdateUser(UpdateUserInfo) returns (google.protobuf.Empty); // 更新用户
  rpc CheckPassword(PasswordCheckInfo) returns (CheckReponse); // 检查密码
  rpc DeleteUser(IdRequest) returns (google.protobuf.Empty); // 删除用户
}

// 密码检查请求消息
message PasswordCheckInfo {
  string password = 1;  // 原始密码
  string encryptedPassword = 2;  // 加密后的密码
}

// 密码检查响应消息
message CheckReponse {
  bool success = 1;  // 检查是否成功
}

// 更新用户信息请求消息
message UpdateUserInfo {
  int32 id = 1;  // 用户ID
  string nickName = 2;  // 昵称
  string gender = 3;  // 性别
  uint64 birthDay = 4;  // 生日
}

// 创建用户信息请求消息
message CreateUserInfo {
  string nickName = 1;  // 昵称
  string passWord = 2;  // 密码
  string mobile = 3;  // 手机号
}

// 分页信息消息
message PageInfo {
  uint32 pn = 1;  // 当前页码
  uint32 pSize = 2;  // 每页大小
}

// 手机号查询请求消息
message MobileRequest {
  string mobile = 1;  // 手机号
}

// 用户ID查询请求消息
message IdRequest {
  int32 id = 1;  // 用户ID
}

// 用户信息响应消息
message UserInfoResponse {
  int32 id = 1;  // 用户ID
  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;  // 用户信息列表
}

5. 用户服务service接口的实现

package handler

import (
	"context"
	"crypto/sha512"
	"fmt"
	"github.com/anaskhan96/go-password-encoder"
	"github.com/golang/protobuf/ptypes/empty"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/types/known/emptypb"
	"gorm.io/gorm"
	"mxshop_server/usr_srv/global"
	"mxshop_server/usr_srv/model"
	"strings"
	"time"
)
import "mxshop_server/usr_srv/model/proto"

type UserServer struct {
}

// ModelToResponse 将 model.User 转换为 proto.UserInfoResponse
func ModelToResponse(user model.User) proto.UserInfoResponse {
	// 在 gRPC 的 message 中字段有默认值,你不能随便赋值 nil 进去,容易出错
	userInfoRsp := proto.UserInfoResponse{
		Id:       user.ID,          // 用户 ID
		PassWord: user.Password,    // 用户密码
		Mobile:   user.Mobile,      // 用户手机号
		NickName: user.Nickname,    // 用户昵称
		Gender:   user.Gender,      // 用户性别
		Role:     int32(user.Role), // 用户角色
	}
	if user.Birthday != nil {
		userInfoRsp.BirthDay = uint64(user.Birthday.Unix()) // 如果用户生日不为空,则转换为 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 // 如果页码为 0,则默认为第一页
		}
		switch {
		case pageSize > 100:
			pageSize = 100 // 如果每页大小超过 100,则设置为 100
		case pageSize <= 0:
			pageSize = 10 // 如果每页大小不合法(小于等于 0),则设置为默认值 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) // 分页查询用户列表

	for _, user := range users {
		userInfoRsp := ModelToResponse(user)      // 将每个用户信息转换为响应格式
		rsp.Data = append(rsp.Data, &userInfoRsp) // 将用户信息添加到响应中
	}
	return rsp, nil // 返回用户列表响应
}

// GetUserByMobile 根据手机号获取用户信息
func (s *UserServer) GetUserByMobile(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.Errorf(codes.NotFound, "用户不存在")
	}
	if result.Error != nil {
		return nil, result.Error
	}
	userInfoRsp := ModelToResponse(user)
	return &userInfoRsp, nil
}

// GetUserById 根据用户ID获取用户信息
func (s *UserServer) GetUserById(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.Errorf(codes.NotFound, "用户不存在")
	}
	if result.Error != nil {
		return nil, result.Error
	}
	userInfoRsp := ModelToResponse(user)
	return &userInfoRsp, nil
}

// CreateUser 创建新用户
func (s *UserServer) CreateUser(ctx context.Context, req *proto.CreateUserInfo) (*proto.UserInfoResponse, error) {
	var user model.User
	result := global.DB.Where(&model.User{Mobile: req.Mobile}).First(&user)
	if result.RowsAffected == 1 {
		return nil, status.Errorf(codes.AlreadyExists, "用户已存在")
	}
	user = model.User{
		Nickname: req.NickName,
		Mobile:   req.Mobile,
		Password: req.PassWord,
	}
	//密码加密
	// 配置密码选项
	options := &password.Options{16, 100, 32, sha512.New}
	// 生成加密密码
	salt, encodePwd := password.Encode(req.PassWord, options)
	user.Password = fmt.Sprintf("$pbkdf2-sha512$%s$%s", salt, encodePwd)

	result = global.DB.Create(&user)
	if result.Error != nil {
		return nil, status.Errorf(codes.Internal, result.Error.Error())
	}
	userInfoRsp := ModelToResponse(user)
	return &userInfoRsp, nil
}

// UpdateUser 更新用户信息
func (s *UserServer) UpdateUser(ctx context.Context, req *proto.UpdateUserInfo) (*emptypb.Empty, error) {
	var user model.User
	result := global.DB.First(&user, req.Id)
	if result.RowsAffected == 0 {
		return nil, status.Errorf(codes.NotFound, "用户不存在")
	}
	if result.Error != nil {
		return nil, result.Error
	}
	birthDay := time.Unix(int64(req.BirthDay), 0)
	user = model.User{
		Nickname: req.NickName,
		Gender:   req.Gender,
		Birthday: &birthDay,
	}
	result = global.DB.Save(&user)
	if result.Error != nil {
		return nil, status.Error(codes.Internal, result.Error.Error())
	}
	return &empty.Empty{}, nil
}

// CheckPassword 检查密码是否正确
func (s *UserServer) CheckPassword(ctx context.Context, req *proto.PasswordCheckInfo) (*proto.CheckReponse, error) {
	// 配置密码选项
	options := &password.Options{16, 100, 32, sha512.New}
	// 拆分加密密码信息
	passwordInfo := strings.Split(req.EncryptedPassword, "$")
	fmt.Println(passwordInfo)
	// 验证密码
	check := password.Verify(req.Password, passwordInfo[2], passwordInfo[3], options)
	return &proto.CheckReponse{Success: check}, nil
}

// DeleteUser 根据用户ID删除用户信息
func (s *UserServer) DeleteUser(ctx context.Context, req *proto.IdRequest) (*emptypb.Empty, error) {
	var user model.User
	result := global.DB.First(&user, req.Id)
	if result.RowsAffected == 0 {
		return nil, status.Errorf(codes.NotFound, "用户不存在")
	}
	if result.Error != nil {
		return nil, status.Error(codes.Internal, result.Error.Error())
	}
	global.DB.Delete(&user)
	return &emptypb.Empty{}, nil
}


6. 注册grpc服务+flag初始化ip和端口地址

package main

import (
	"flag"                          // 导入 flag 包,用于解析命令行参数
	"fmt"                           // 导入 fmt 包,用于格式化输出
	"google.golang.org/grpc"        // 导入 gRPC 包
	"mxshop_server/usr_srv/handler" // 导入自定义的处理程序包
	"mxshop_server/usr_srv/proto"   // 导入自动生成的 gRPC 协议文件包
	"net"                           // 导入 net 包,用于网络操作
)

func main() {
	IP := flag.String("ip", "0.0.0.0", "ip地址") // 定义一个字符串类型的命令行参数 -ip,默认值为 "0.0.0.0",用于指定 IP 地址
	Port := flag.Int("port", 50051, "端口号")     // 定义一个整数类型的命令行参数 -port,默认值为 50051,用于指定端口号

	flag.Parse()                 // 解析命令行参数
	fmt.Println("ip: ", *IP)     // 输出 IP 地址
	fmt.Println("port: ", *Port) // 输出端口号

	server := grpc.NewServer()                                      // 创建一个 gRPC 服务器实例
	proto.RegisterUserServer(server, &handler.UserServer{})         // 注册用户服务到 gRPC 服务器,handler.UserServer{} 实现了 proto.UserServer 接口
	lis, err := net.Listen("tcp", fmt.Sprintf("%s:%d", *IP, *Port)) // 在指定 IP 地址和端口号上监听 TCP 连接
	if err != nil {
		panic("failed to listen:" + err.Error()) // 如果监听失败,输出错误信息并退出程序
	}
	err = server.Serve(lis) // 启动 gRPC 服务器,开始接受客户端连接
	if err != nil {
		panic("failed to start grpc:" + err.Error()) // 如果启动失败,输出错误信息并退出程序
	}
}

7. 测试用例

package main

import (
	"context"
	"fmt"
	"google.golang.org/grpc"
	"mxshop_server/usr_srv/proto"
	"time"
)

var userClient proto.UserClient // 用户客户端
var conn *grpc.ClientConn       // gRPC 连接

// Init 初始化函数,建立与 gRPC 服务的连接
func Init() {
	var err error
	// 建立与 gRPC 服务的连接
	conn, err = grpc.Dial("localhost:50051", grpc.WithInsecure())
	if err != nil {
		panic(err)
	}
	userClient = proto.NewUserClient(conn)
}

// TestGetUserList 测试函数,获取用户列表信息
func TestGetUserList() {
	// 调用 gRPC 服务的 GetUserList 方法获取用户列表信息
	rsp, err := userClient.GetUserList(context.Background(), &proto.PageInfo{
		Pn:    1,
		PSize: 10,
	})
	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)
	}
}

// TestCreateUser 测试函数,创建用户
func TestCreateUser() {
	// 循环创建用户
	for i := 0; i < 10; i++ {
		// 调用 gRPC 服务的 CreateUser 方法创建用户
		rsp, err := userClient.CreateUser(context.Background(), &proto.CreateUserInfo{
			NickName: fmt.Sprintf("bobby%d", i),
			Mobile:   fmt.Sprintf("1878222222%d", i),
			PassWord: "admin123",
		})
		if err != nil {
			panic(err)
		}
		fmt.Println(rsp.Id)
	}
}

// TestUpdateUser 测试函数,更新用户信息
func TestUpdateUser() {
	// 调用 gRPC 服务的 UpdateUser 方法更新用户信息
	rsp, err := userClient.UpdateUser(context.Background(), &proto.UpdateUserInfo{
		Id:       1,
		NickName: "bobby2333",
		Gender:   "male",
		BirthDay: uint64(time.Now().Unix()),
	})
	if err != nil {
		panic(err)
	}
	fmt.Println(rsp)
}

// TestDeleteUser 测试函数,删除用户
func TestDeleteUser() {
	// 调用 gRPC 服务的 DeleteUser 方法删除用户
	rsp, err := userClient.DeleteUser(context.Background(), &proto.IdRequest{
		Id: 1,
	})
	if err != nil {
		panic(err)
	}
	fmt.Println(rsp)
}

// TestGetUserById 测试函数,通过用户ID获取用户信息
func TestGetUserById() {
	// 调用 gRPC 服务的 GetUserById 方法通过用户ID获取用户信息
	rsp, err := userClient.GetUserById(context.Background(), &proto.IdRequest{
		Id: 1,
	})
	if err != nil {
		panic(err)
	}
	fmt.Println(rsp)
}

func main() {
	// 初始化连接
	Init()

	// 运行测试函数
	TestGetUserList()
	TestCreateUser()
	TestUpdateUser()
	TestGetUserById()
	TestDeleteUser()

	// 关闭连接
	defer conn.Close()
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值