【Go】九、API 编写测试_实现一个用户模块的接口

项目构建

New Project 直接创建项目,只需要起名字,之后在根目录中创建对应的微服务,这里先开发用户微服务模块:

mxshop_srvs

user_srv

global 公共内容

handler 服务

model 数据模型(表结构对应的模型)

proto 暴露接口

main.go 用来启动程序

数据模型创建

在 model 目录下 创建对应的文件:

model

user.go

main

main.go

user.go:

package modelimport ("time""gorm.io/gorm"
)/*
*
公共表内容,所有的表都应该具有这几项,其他的 struct 都应该继承这个 struct
*/
type BaseModel struct {ID        int32     `gorm:"primarykey"`CreatedAt time.Time `gorm:"column:add_time"`UpdatedAt time.Time `gorm:"column:update_time"`DeletedAt gorm.DeletedAtIsDeleted bool
}type User struct {BaseModelMobile   string     `gorm:"index:idx_mobile;unique;type:varchar(11);not null"` // 为手机号添加索引、唯一约束、11位以下、非空Password string     `gorm:"type:varchar(100);not null"`                        // 设置为 100 主要是为了后面的 md5 加密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     int        `gorm:"column:role;default:1;type:int comment '1 表示普通用户,2 表示管理员'"`
}

同目录下的 main/main.go 的作用是用来连接数据库并调用 gorm 的模型进行建表

注意这里需要提前建库,库的语言选择:utf8mb4_general_ci

main.go

package mainimport ("log""os""time""gorm.io/driver/mysql""gorm.io/gorm""gorm.io/gorm/logger""gorm.io/gorm/schema""mxshop_srvs/user_srv/model"
)func main() {dsn := "root:你的密码@tcp(你的ip:你的端口)/你的库名?charset=utf8mb4&parseTime=True&loc=Local"// 添加日志信息newLogger := logger.New(log.New(os.Stdout, "\r\n", log.LstdFlags), // io writerlogger.Config{SlowThreshold:             time.Second, // Slow SQL thresholdLogLevel:                  logger.Info, // Log levelIgnoreRecordNotFoundError: true,        // Ignore ErrRecordNotFound error for loggerParameterizedQueries:      true,        // Don't include params in the SQL logColorful:                  true,        // Disable color,true is colorful, false to black and white},)db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{// 阻止向创建的数据库表后添加复数NamingStrategy: schema.NamingStrategy{SingularTable: true,},// 将日添加到配置中Logger: newLogger,})if err != nil {panic(err)}// 建表,此处导入的是之前创建的 model_ = db.AutoMigrate(&model.User{})
}

这个 main 文件顺利执行之后,数据库中的表就建好了,此处应该是创建好了一个 user 表

md5 加密

密码的存储需要使用 md5 加密后再进行存储,至于为什么要这么做不再赘述,这里只介绍流程:

func genMd5(code string) string {Md5 := md5.New()_, _ = io.WriteString(Md5, code)return hex.EncodeToString(Md5.Sum(nil))
}func main() {fmt.Println(genMd5("123456"))
}

但是注意,这种 常规的 md5 加密模式对于简单的数字组合已经基本完成暴力解密了,所以当用户输入简单密码,例如:123456 这种时,已经完全可以被拦截到密码原文了,故而产生了进一步的 MD5 加密算法,MD5 盐值加密。

MD5 盐值加密也就是 将密码原文中添加一串随机数,再进行 MD5 加密

这里我们调用开源项目:go-password-encoder 进行盐值加密:

其中涉及到了向数据库中添加的密码问题,其策略是将加密方式、盐值、md5密码全部存储进数据库,再从数据库中取出来进行解析

package mainimport ("crypto/md5""crypto/sha512""encoding/hex""fmt""github.com/anaskhan96/go-password-encoder""io""strings"
)func genMd5(code string) string {Md5 := md5.New()_, _ = io.WriteString(Md5, code)return hex.EncodeToString(Md5.Sum(nil))
}func main() {fmt.Println(genMd5("123456"))options := &password.Options{16, 100, 32, sha512.New}salt, encodedPwd := password.Encode("generic password", options)// 采取将 加密算法、盐值、加密后的密码 保存到数据库中newPassword := fmt.Sprintf("%s$%s$%s", "pbkdf2-sha512", salt, encodedPwd)passwordInfo := strings.Split(newPassword, "$")fmt.Println(passwordInfo)check := password.Verify("generic password", passwordInfo[1], passwordInfo[2], options)fmt.Println(check)
}

Proto

在提前建立好的 proto 目录下 创建对应的user.proto 文件

proto

user.proto

user.pb.go

user_grpc.pb.go

编写 .proto 文件:

syntax = "proto3";
import "google/protobuf/empty.proto";
option go_package = ".;proto";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);   // 更新用户,此处返回空,要引入基础包中的 google.protobuf.emptyrpc 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 IdRequest {int32 id = 1;
}message MobileRequest {string mobile = 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;
}

并调用对应的命令生成对应的 go 语言源码

我们在生成的 xxx_grpc.pb.go 的 xxxServer 中可以找到我们需要实现的接口进行实现

接口实现

接口的实现在 handler 目录下新建 .go 文件进行实现:

handler

user.go

注意这里先定义一下 gorm 的初始化工作:

在 global 目录中定义 init() 方法,该方法会自动被调用

global

global.go

global.go

package globalimport ("gorm.io/driver/mysql""gorm.io/gorm""gorm.io/gorm/logger""gorm.io/gorm/schema""log""os""time"
)var (DB *gorm.DB
)/*
*
注意:名称被定义为 init() 的韩式会在 main() 之前被调用
*/
func init() {dsn := "root:VBwH9eW*urHb@tcp(192.168.202.140:3306)/mxshop_user_srv?charset=utf8mb4&parseTime=True&loc=Local"// 添加日志信息newLogger := logger.New(log.New(os.Stdout, "\r\n", log.LstdFlags), // io writerlogger.Config{SlowThreshold:             time.Second, // Slow SQL thresholdLogLevel:                  logger.Info, // Log levelIgnoreRecordNotFoundError: true,        // Ignore ErrRecordNotFound error for loggerParameterizedQueries:      true,        // Don't include params in the SQL logColorful:                  true,        // Disable color,true is colorful, false to black and white},)var err errorDB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{// 阻止向创建的数据库表后添加复数NamingStrategy: schema.NamingStrategy{SingularTable: true,},// 将日添加到配置中Logger: newLogger,})if err != nil {panic(err)}
}

GetUserList 接口

之后在 user.go 中实现接口:

package handlerimport ("context""gorm.io/gorm""mxshop_srvs/user_srv/global""mxshop_srvs/user_srv/model""mxshop_srvs/user_srv/proto"
)type UserServer struct{}/*
* 优雅的分页方法*/
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 = 100case pageSize <= 0:pageSize = 10}offset := (page - 1) * pageSizereturn db.Offset(offset).Limit(pageSize)}
}/**
* 转换 model 为 proto 的 UserInfoResponse*/
func ModelToResponse(user model.User) proto.UserInfoResponse {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
}func (s *UserServer) GetUserList(ctx context.Context, req *proto.PageInfo) (*proto.UserListResponse, error) {var users []model.Userresult := global.DB.Find(&users) // result 只是查询的结果(是否出错等),不是最后得到的查询结果集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 {userInfoResponse := ModelToResponse(user)rsp.Data = append(rsp.Data, &userInfoResponse)}return rsp, nil
}

GetUserByxxx 接口

开发流程:

  1. 在自动生成的xxx_grpc.pb.go 中找到需要实现的接口:

    GetUserByMobile(context.Context, *MobileRequest) (*UserInfoResponse, error)
    

    这里就是需要实现的接口

    我们在 handler 目录中 对应的 service 文件中定义新的方法:

    type UserServer struct{}func (s *UserServer) GetUserByMobile(ctx context.Context, req *proto.MobileRequest) (*proto.UserInfoResponse, error) {}

    之后向其中添加对应的逻辑:

  2. 对于 getUserByMobile 涉及到的问题:

    包括:查询出错、未查到信息、错误码等情况

    /*
    *
    通过手机号查询用户
    */
    func (s *UserServer) GetUserByMobile(ctx context.Context, req *proto.MobileRequest) (*proto.UserInfoResponse, error) {// 用来存储取出来的 uservar user model.Userresult := global.DB.Where(&model.User{Mobile: req.Mobile}).First(&user) // 查询一个,将结果录入if result.RowsAffected == 0 {                                           // 若影响行数为 0 ,即没有查到对应的数据return nil, status.Error(codes.NotFound, "用户不存在")}if result.Error != nil { // 若查询出现错误return nil, result.Error}userInfoRsp := ModelToResponse(user)return &userInfoRsp, nil // 走到这里的都成功了,叫做成功
    }
    
  3. 实现 getUserById 方法:

    /**
    * 通过 id 查询用户*/
    func (s *UserServer) GetUserById(ctx context.Context, req *proto.IdRequest) (*proto.UserInfoResponse, error) {var user model.Userresult := 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.Userresult := global.DB.Where(&model.User{Mobile: req.Mobile}).First(&user)if result.RowsAffected == 1 { // 若准备新增的用户已存在return nil, status.Errorf(codes.AlreadyExists, "用户已存在")}user.Mobile = req.Mobileuser.NickName = req.Nickname// 密码加密// 引入两个包:/*** go get github.com/anaskhan96/go-password-encoder	// 这个是外部包,必须引入* "crypto/sha1"		这个包是内部包,可能不需要引入*/options := &password.Options{16, 100, 32, sha1.New}salt, encodedPwd := password.Encode(req.Password, options)newPassword := fmt.Sprintf("$pdkdf2-sha512$%s$%s", salt, encodedPwd)user.Password = newPasswordresult = 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) (*empty.Empty, error) {var user model.Userresult := global.DB.First(&user, req.Id)if result.RowsAffected == 0 {return nil, status.Errorf(codes.NotFound, "用户不存在")}// 将生日转换为时间,请求传入的是 uint64,这里要先转为int64,再转为 Time,才能存储到数据库中birthday := time.Unix(int64(req.BirthDay), 0)user.NickName = req.NickNameuser.Birthday = &birthdayuser.Gender = req.Genderresult = global.DB.Save(user)if result.Error != nil {return nil, status.Errorf(codes.Internal, result.Error.Error()) // 传出错误}return &empty.Empty{}, nil
}

检查密码接口

/*** 检查密码*/
func (s *UserServer) CheckPassWord(ctx context.Context, req *proto.PassWordCheckInfo) (*proto.CheckResponse, error) {options := &password.Options{16, 100, 32, sha1.New}passwordInfo := strings.Split(req.EncryptedPassword, "$")check := password.Verify(req.Password, passwordInfo[2], passwordInfo[3], options)return &proto.CheckResponse{Success: check}, nil
}

服务启动类配置

main.go:

package mainimport ("flag""fmt""net""google.golang.org/grpc""mxshop_srvs/user_srv/handler""mxshop_srvs/user_srv/proto"
)func main() {// 由于ip和端口号有可能需要用户输入,所以这里摘出来// flag 包是一个命令行工具包,允许从命令行中设置参数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("fail to start grpc" + err.Error())}
}

此时我们直接运行 main.go 就会以 0.0.0.0:50051打开服务,但是我们配置了 命令行启动服务的功能,所以我们可以先打包这个项目,再使用命令行进行启动:

cd .\user_srv\
go build main.go

此时会打包出一个 main.exe 文件,此时:

start main.exe

就可以以默认形式启动刚刚的程序

再cmd中调用

main.exe -h

可以查看命令帮助,此处会生成:

Usage of main.exe:-ip stringip地址 (default "0.0.0.0")-port int端口号 (default 50051)

之后再进行调用:

main.exe -port 50053

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/824437.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

NVM下载、NVM配置、NVM常用命令

NVM(nodejs版本管理切换工具)下载、配置、常用命令 0、NVM常用命令 nvm off // 禁用node.js版本管理(不卸载任何东西) nvm on // 启用node.js版本管理 nvm install <version> // 安装node.js的命名 version是版本号 例…

发布!DolphinDB 白皮书正式上线官网!

对广大数据库用户而言&#xff0c;白皮书是极具参考价值的使用指南和学习手册。白皮书不但能深入剖析数据库的基础概念与架构&#xff0c;协助用户了解数据库的工作原理和应用技巧&#xff0c;更提供了丰富的实践案例&#xff0c;帮助用户从中汲取经验&#xff0c;避免在实际应…

【JAVA基础篇教学】第十一篇:Java中字符串操作详解

博主打算从0-1讲解下java基础教学&#xff0c;今天教学第十篇&#xff1a;Java中字符串操作详解。 在 Java 编程中&#xff0c;字符串是一种常见的数据类型&#xff0c;通常用于存储文本信息。Java 提供了丰富的字符串操作方法&#xff0c;可以对字符串进行分割、截取、查找…

【QT进阶】Qt Web混合编程之VS2019 CEF的编译与使用(图文并茂超详细介绍)

往期回顾 【QT入门】Qt自定义控件与样式设计之自定义QLineEdit实现搜索编辑框-CSDN博客 【QT入门】Qt自定义控件与样式设计之自定义QTabWidget实现tab在左&#xff0c;文本水平的效果-CSDN博客【QT进阶】Qt Web混合编程之CEF、QCefView简单介绍-CSDN博客 【QT进阶】Qt Web混合编…

【学习笔记】天元西南中心的线上课程《理解神经网络》

机器学习介绍 introduction machine learning deep learning statistics vs. machine learning 区别&#xff1a;是否存在模型依赖&#xff1b;模型实质由参数组成。 statistics: model based 用训练数据来构建模型&#xff0c;是为了获取模型的参数&#xff0c;参数就是模…

文件msvcr120.dll丢失怎样修复?这三种方法能准确修复msvcr120.dll

小编为大家总结了解决msvcr120.dll文件缺失问题的三种方法&#xff0c;以帮助你快速解决这一难题。首先&#xff0c;我们来看看msvcr120.dll文件为何会出现丢失的情形。 一.msvcr120.dll丢失问题的常见原因包括 病毒感染&#xff1a;病毒或恶意软件侵入电脑有可能会损毁或删除…

【第1节】书生·浦语大模型全链路开源开放体系

目录 1 简介2 内容&#xff08;1&#xff09;书生浦语大模型发展历程&#xff08;2&#xff09;体系&#xff08;3&#xff09;亮点&#xff08;4&#xff09;全链路体系构建a.数据b 预训练c 微调d 评测e.模型部署f.agent 智能体 3 相关论文解读4 ref 1 简介 书生浦语 InternLM…

.net9 AOT编绎生成标准DLL,输出API函数教程-中国首创

1&#xff0c;安装VS2022预览版&#xff08;Visual Studio Preview&#xff09; https://visualstudio.microsoft.com/zh-hans/vs/preview/#download-preview 2&#xff0c;选择安装组件&#xff1a;使用C的桌面开发 和 .NET桌面开发 ------------------------------------- …

SnapGene Mac激活版 分子生物学软件

SnapGene Mac是一款功能全面、操作便捷的综合性分子生物学软件&#xff0c;专为Mac用户打造。它集成了DNA序列编辑、分析、可视化和团队协作等多种功能&#xff0c;为科研人员提供了一个高效、可靠的分子生物学研究工具。 SnapGene Mac激活版下载 在SnapGene Mac中&#xff0c;…

批量插入10w数据方法对比

环境准备(mysql5.7) CREATE TABLE user (id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 唯一id,user_id bigint(10) DEFAULT NULL COMMENT 用户id-uuid,user_name varchar(100) NOT NULL COMMENT 用户名,user_age bigint(10) DEFAULT NULL COMMENT 用户年龄,create_time time…

Docker搭建Minisatip

Minisatip 是一个多线程的 satip 服务器版本 1.2&#xff0c;它在 Linux 下运行&#xff0c;并且已经通过了与 DVB-S、DVB-S2、DVB-T、DVB-T2、DVB-C 等的测试。satip 是一个用于接收卫星电视流的服务器软件&#xff0c;而 Minisatip 则是该软件的一个轻量级版本&#xff0c;它…

ssm057学生公寓管理中心系统的设计与实现+jsp

学生公寓管理中心系统设计与实现 摘 要 现代经济快节奏发展以及不断完善升级的信息化技术&#xff0c;让传统数据信息的管理升级为软件存储&#xff0c;归纳&#xff0c;集中处理数据信息的管理方式。本学生公寓管理中心系统就是在这样的大环境下诞生&#xff0c;其可以帮助管…

JS版本号比较大小

概述 在项目开发和运行的过程中&#xff0c;总是少不了各类升级。例如某个功能组件需要更高的依赖库、数据项需要进行兼容等等问题。遇到此类问题开发者需要使用版本号来解决。版本号通常由三部分组成&#xff1a;主版本号、次版本号和修订版本号 通常来说升级会涉及到三个…

【JavaEE多线程】线程的创建

系列文章目录 &#x1f308;座右铭&#x1f308;&#xff1a;人的一生这么长、你凭什么用短短的几年去衡量自己的一生&#xff01; &#x1f495;个人主页:清灵白羽 漾情天殇_计算机底层原理,深度解析C,自顶向下看Java-CSDN博客 ❤️相关文章❤️&#xff1a;清灵白羽 漾情天…

【代码】Python3|Requests 库怎么继承 Selenium 的 Headers (2024,Chrome)

本文使用的版本&#xff1a; Chrome 124Python 12Selenium 4.19.0 版本过旧可能会出现问题&#xff0c;但只要别差异太大&#xff0c;就可以看本文&#xff0c;因为本文对新老版本都有讲解。 文章目录 1 难点解析和具体思路2 注意事项2.1 PDF 资源获取时注意事项2.2 Capabiliti…

asp.net core 依赖注入后的服务生命周期

ASP.NET Core 依赖注入&#xff08;DI&#xff09;容器支持三种服务的生命周期选项&#xff0c;它们定义了服务实例的创建和销毁的时机。理解这三种生命周期对于设计健壯且高效的应用程序非常重要&#xff1a; 瞬时&#xff08;Transient&#xff09;&#xff1a; 瞬时服务每次…

西瓜书学习——第一、二章笔记

[] 什么是机器学习? 研究关于“学习算法”(一类能从数据中学习出其背后潜在规律的算法)的一门学科。 PS:深度学习指的是神经网络那一类学习算法&#xff0c;因此是机器学习的子集。 假设空间和版本空间 举个栗子:假设现已收集到某地区近几年的房价和学校数量数据&#xf…

CloudCompare 整体架构

CloudCompare 是一个开源的大规模点云处理软件,内置了点云渲染和各种点云算法,还有插件化拓展,非常适合作为点云相关软件的基础程序进行二次开发。源码地址:https://github.com/CloudCompare/CloudCompare 这里记录一下项目的主要模块目录和他们做的事情。 编译和依赖 编…

ChatGPT及GIS、生物、地球、农业、气象、生态、环境科学领域案例

以ChatGPT、LLaMA、Gemini、DALLE、Midjourney、Stable Diffusion、星火大模型、文心一言、千问为代表AI大语言模型带来了新一波人工智能浪潮&#xff0c;可以面向科研选题、思维导图、数据清洗、统计分析、高级编程、代码调试、算法学习、论文检索、写作、翻译、润色、文献辅助…

SRIO系列-时钟逻辑与复位逻辑

一、前言 上一篇讲述了SRIO协议的基本概念&#xff0c;传输的HELLO帧格式、事务类型等&#xff0c;本篇说一下SRIO IP核的时钟关系。 基本的IP设置可以参考此篇文章&#xff1a;【高速接口-RapidIO】Xilinx SRIO IP 核详解-CSDN博客 二、时钟关系 PHY可以在两个时钟域上运行…