做违规网站/抖音推广合作方式
文章目录
- 用户微服务
- 表结构
- 查表
- web 服务
- 跨域问题
- 图形验证码
- 短信
- 用户注册
- 服务中心
- 注册 grpc 服务
- 动态获取端口
- 负载均衡
- 配置中心
- 启动项目
- 小结
用户微服务
- 作为系统的第一个微服务,开发的技术点前面已经了解了一遍,虽有待补充,但急需实战
- 这里主要梳理架构和开发步骤,为整个开发定下基调
- go version 升到 1.20
表结构
- 在 model 定义用户表结构;技术点:gorm
- 密文保存密码,使用信息摘要算法 md5 配合盐值
- 使用工具,可指定使用的算法,验证密码
- 采用比较普遍的做法:将算法、盐值、密码拼接存入数据库
- 将表创建:
_ = db.AutoMigrate(&model.User{})
查表
- 在 proto 文件定义需要的方法,请求参数,返回值;技术点:protobuf
- 主要是数据库相关的操作
- 生成代码,handler 中实现接口中定义的方法,即 server 端逻辑
protoc -I . user.proto --go_out=plugins=grpc:.
- 返回值一定要严格按照 message 定义
type UserServer interface {GetUserList(context.Context, *PageInfo) (*UserListResponse, error)GetUserByMobile(context.Context, *MobileRequest) (*UserInfoResponse, error)GetUserById(context.Context, *IdRequest) (*UserInfoResponse, error)CreateUser(context.Context, *CreateUserInfo) (*UserInfoResponse, error)UpdateUser(context.Context, *UpdateUserInfo) (*emptypb.Empty, error)CheckPassWord(context.Context, *PasswordCheckInfo) (*CheckResponse, error) }
- GetUserList/GetUserById
- 使用 gorm 的 DB struct 和分页方法
- 定义将 model struct 转换为 message response 的函数
- GetUserByMobile
- 使用 config + viper 配合 yaml 文件做全局配置,可以回顾前面的文章
- 使用 grpc 的 status 返回状态信息,日志和错误处理很重要
- UpdateUser
- 处理时间:
time.Unix(int64(req.BirthDay)
- 处理时间:
- 验证密码
- 使用工具的
Verify
函数
- 使用工具的
web 服务
- 上面的用户微服务是 service 层,接下来通过 gin 将 service 层的 api 暴露出来,也就是 client 端 API
- 当然,先从 user-web 开始,api/user.go
- 也可以通过 grpc 的 gateway 暴露
- 日志配置,使用 zap 将日志输出到文件,可以回顾前面的文章,或者看官方教程
- 用 Sugar 会损失一些性能(反射判断类型),但是好用
- 配置
zap.S()
- 初始化放在 initialize
- 初始化
Router := gin.Default()
,只能有一个,通过 Group 添加分组 - 初始化日志
- 把server 端的 proto 文件拿过来,生成 stub,开始编写客户端调用;client 端和页面在这里是前端
- 转换 grpc status 为 HTTP status code
- 在 global/user.go 定义格式
- 得到 server 端的 response 可以直接用
map[sting]interface{}
存用户信息,但是切片不够优雅 - 为前端定义 UserResponse struct,实例化 user 再加入
[]interface{}
- 为 birthday 重写
MarshalJSON
方法,返回指定的时间格式
- 得到 server 端的 response 可以直接用
- 初始化
- 配置文件管理工具;技术点:viper
- 将文件解析成内部的 struct 是 go 语言中非常常见的操作,上面为返回值定义 struct 也是一样的道理,在 go 环境中操作,自然是 struct 更方便;定义 struct 时打 tag 也是这个作用,把文件/json体中的数据映射成 struct 字段值
- viper 可以自动将 yaml 文件解析成 struct
- 将线上和线下配置文件隔离:在本地设置环境变量,比如
SHOP_DEBUG=true
,代表开发环境func GetEnvInfo(env string) bool {viper.AutomaticEnv()return viper.GetBool(env)//刚才设置的环境变量 想要生效 我们必须得重启goland }
- 服务器启动后,使用
v.WatchConfig()
配合fsnotify
监听配置文件变化v.AddConfigPath(configFileName) viper.WatchConfig() // 要放在 AddConfigPath 之后 viper.OnConfigChange(func(e fsnotify.Event) {fmt.Println("Config changed.", e.Name) }) err := viper.ReadConfig() err := viper.Unmarshal()
- 后面会引入分布式服务的配置中心
- 接口(API)和前端页面都可以通过 yapi 做 mock 测试,有的接口也可以写 UT 测试,可以回顾前面的文章
- 一些组件的使用方法可以临时新建目录写 demo 测试
- 注:初始化操作的调用和配置文件的加载都是在
main.go
,入口嘛
- 密码登录 PassWordLogin
- 表单验证,可以回顾前面的文章,这部分都是用验证器实现
- 初始化 validator 中实现本地化(多语言),通过修改
binding.Validator
引擎的属性 - 自定义手机号验证器并注册到
gin/binding
(注册个 tag),表单字段的验证也是用这个;也要给自定义的验证器注册翻译器,binding 中定义好的验证器(比如 required)已经通过修改引擎实现了翻译import ("regexp"// 用 v10"github.com/go-playground/validator/v10" )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 }
// main.go //注册validator/中的验证器,验证手机号 if v, ok := binding.Validator.Engine().(*validator.Validate); ok {_ = v.RegisterValidation("mobile", myvalidator.ValidateMobile)_ = v.RegisterTranslation("mobile", global.Trans, func(ut ut.Translator) error {return ut.Add("mobile", "{0} 非法的手机号码!", true) // see universal-translator for details}, func(ut ut.Translator, fe validator.FieldError) string {t, _ := ut.T("mobile", fe.Field())return t}) }
- 验证密码的逻辑就是先根据手机号查询,再比对密码;使用 proto 中的方法(已经在server端定义好),注意参数形式,对照 proto 文件写
- 关键在于验证通过后的处理,使用 sessionId 还是 token?
- session 机制
- 在单体应用中,登录成功会返回 sessionId 存在 cookie,表明身份
- 在微服务中,由于服务之间隔离,在用户服务保存的 sessionid 不能在商品服务验证,需要有个公用的数据库
- 但我们不用上面的方案,而是采用更方便的 JWT(json web token)
- 在单体应用中,登录成功会返回 sessionId 存在 cookie,表明身份
- JWT
- 基础知识
- 一般放在浏览器的 Headers 中,可以自定义名称,比如
x-token
- JWT 相关代码是通用的,放在 middlewares下面;models 下定义 payload struct
- payload 不能放敏感信息,因为是没有加密的,我们只是通过 Signature 部分判断 token 有效性(secret 只存在服务器,一定不能泄露;类似对称加密,私钥签私钥解)
- 如何保证JWT的安全呢?一般会把过期时间设置短一些
- 可以在官网测试一下生成的 token
- 然后给 URL 添加 token 验证
- 使用鉴权函数
JWTAuth()
,因为是作为中间件要注册的,所以写法上是返回函数return func(c *gin.Context)
- 鉴权后使用
c.Set()
将登录用户的信息保存在 context,方便后端获取 - URL 的鉴权在 router 中添加
middlewares.JWTAuth()
即可 - 如果是个整个 Group 添加,直接用
.Use(middlewares.JWTAuth())
- 使用鉴权函数
- 验证是否为管理员
- 还是在 middlewares 定义鉴权函数,用刚保存的 context 获取
AuthorityId
,如果为 2 则是管理员 - Role Id 是在登录时就从数据库获取并保存在 token
- 可以做基于 Role 的访问控制(RBAC),对访问 API 做控制
- 还是在 middlewares 定义鉴权函数,用刚保存的 context 获取
- 更多注释在代码中,可以 clone 代码研究
跨域问题
- 跨域(protocol/ip/port 不一致)一般会报 404
- 在发起复杂请求且跨域时(这种情况很常见),浏览器出于安全性考虑,会发起
OPTIONS
预检请求- 预检是 CORS(跨域资源共享) 中一种透明服务器验证机制。预检请求首先需要向另外一个域名的资源发送一个 HTTP OPTIONS 请求头,其目的是为了判断实际发送的请求是否是安全的
- 测试代码,本地请求用户列表,会跨域
<!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">请求数据</button><div id="content" style="background-color: aquamarine; width: 300px;height: 500px;"></div> </body> <script type="text/javascript">$("#query").click(function () {$.ajax({url:"http://127.0.0.1:8021/u/v1/user/list",dataType: "json",type: "get",beforeSend: function(request) {request.setRequestHeader("x-token", "eyJhbGciOiJIUzI1NiIsInR5cC")},success: function (result) {console.log(result.data);$("#content").text(result.data);},error: function (data) {alert("请求出错")});}); </script> </html>
- 跨域可以在前端或者后端解决
- 这里我们在后端解决,还是使用中间件 cors.go,设置 Header 字段
Access-Control-Allow-Methods
package middlewaresimport ("github.com/gin-gonic/gin""net/http" )func Cors() gin.HandlerFunc {return func(c *gin.Context) {method := c.Request.Methodc.Header("Access-Control-Allow-Origin", "*")c.Header("Access-Control-Allow-Headers", "Content-Type,AccessToken,X-CSRF-Token, Authorization, Token, x-token")c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PATCH, PUT")c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type")c.Header("Access-Control-Allow-Credentials", "true")// 不需要预检,直接 abort,再请求原地址即可if method == "OPTIONS" {c.AbortWithStatus(http.StatusNoContent)}} }
- 在初始化时配置跨域:
Router.Use(middlewares.Cors())
图形验证码
- 参考文档,相关逻辑放在 api/captcha.go
- 这块路由我们新建 BaseRouter
- 全局变量
store
,is a shared storage for captchas,同一个包(目录)内的 go 文件都可以用- 提供了 Verify 方法,验证通过 form 传过来的验证码
- 在登录表单中加上 Captcha 和 CaptchaId
- 验证之后当前验证码就会失效,已经从变量中删除
短信
- 用户注册或动态登录需要短信验证码
- 使用阿里云的服务,去控制台申请 code,代码是固定的
- 添加配置
type RedisConfig struct {Host string `mapstructure:"host" json:"host"`Port int `mapstructure:"port" json:"port"`Expire int `mapstructure:"expire" json:"expire"` // 短信过期时间 } type AliSmsConfig struct {ApiKey string `mapstructure:"key" json:"key"`ApiSecrect string `mapstructure:"secrect" json:"secrect"` }
- 把发出去的验证码存在 Redis,也能设置过期时间
rdb.Set(context.Background(), sendSmsForm.Mobile, smsCode, time.Duration(global.ServerConfig.RedisInfo.Expire)*time.Second)
- 给前端返回 “发送成功” 即可
- 前端部分,用户输入验证码,自然需要定义表单了
- 一般咱们写 API 的逻辑是从前端到后端,便于明确需求和参数
- 当然,前端页面也要结合后端的设计,但总的来说应该先有前端的需求和项目的架构,绘制 Draft 界面
- 以上是
SendSms
接口,给前端调的,但依托于两处验证,才会用到:动态登录,用户注册
用户注册
- 最后一个 API
- 前端需要接收用户的注册参数,client 端调用后端的 CreateUser 即可,我们从定义表单开始
- 采用注册成功自动登录的逻辑,就不跳转到登录页面让用户再次输入了
- 在这里创建 JWT,并返回和密码登录逻辑一样的数据即可
// 返回给这些值就代表登录成功 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, })
- 用户发起 ajax 请求并根据返回字段判断是否登录成功
- 在这里创建 JWT,并返回和密码登录逻辑一样的数据即可
服务中心
- 有很多服务注册和发现的工具,这里我们选择 consul,支持健康检查和 DNS
- 前面的文章中整理过,可以写 python 代码测试 consul 的接口,也可以使用 postman
- 这篇文章底层使用 python 编写 grpc 服务,但道理是相通的
- 用 go 调用一下 consul 的各接口,方便后续接入
package mainimport ("fmt""github.com/hashicorp/consul/api" )func Register(address string, port int, name string, tags []string, id string) error {cfg := api.DefaultConfig()cfg.Address = "192.168.1.103:8500"client, err := api.NewClient(cfg)if err != nil {fmt.Println(err)}//生成对应的检查对象check := &api.AgentServiceCheck{HTTP: "http://192.168.1.102:8021/health",Timeout: "5s",Interval: "5s",DeregisterCriticalServiceAfter: "10s",}//生成注册对象// 两种方式//registration := api.AgentServiceRegistration{// Kind: "",// ID: "",// Name: "",// Tags: nil,// Port: 0,// Address: "",// SocketPath: "",// TaggedAddresses: nil,// EnableTagOverride: false,// Meta: nil,// Weights: nil,// Check: nil,// Checks: nil,// Proxy: nil,// Connect: nil,// Namespace: "",// Partition: "",//}// 我们使用 new 实例化structregistration := new(api.AgentServiceRegistration)registration.Name = nameregistration.ID = idregistration.Port = portregistration.Tags = tagsregistration.Address = addressregistration.Check = checkerr = client.Agent().ServiceRegister(registration)// client.Agent().ServiceDeregister()if err != nil {fmt.Println(err)}return nil }func AllServices() {cfg := api.DefaultConfig()cfg.Address = "192.168.1.103:8500"client, err := api.NewClient(cfg)if err != nil {panic(err)}data, err := client.Agent().Services()if err != nil {panic(err)}for key, _ := range data {fmt.Println(key)} } func FilterSerivice() {cfg := api.DefaultConfig()cfg.Address = "192.168.1.103:8500"client, err := api.NewClient(cfg)if err != nil {panic(err)}data, err := client.Agent().ServicesWithFilter(`Service == "user-web"`)if err != nil {panic(err) // 如果报错不用管,IDE 的原因}for key, _ := range data {fmt.Println(key)} }func main() {_ = Register("192.168.1.102", 8021, "user-web", []string{"vshop", "Roy"}, "user-web")//AllServices()//FilterSerivice()// fmt.Println(fmt.Sprintf(`Service == "%s"`, "user-srv")) }
- 服务没启动时,会显示这个检查结果
注册 grpc 服务
- 之前 user-web 通过
grpc.Dial
直接拨号连接 service 层的服务,现在用服务发现注册中心代替- 当然,Dial 是必须的,这里只不过是将 IP 和 Port 集中管理,查找获取,不再是写死的
- 先服务注册,将 grpc 服务(service层)注册到 consul 中
- GRPC 或 HTTP 都是在
AgentServiceCheck
里指明;其实主要区别就是 consul 对它们健康检查的方式不一样,获取服务不都是IP/Port嘛
// 这是HTTP的检查方式,很简单,请求 /health 能返回即可 // 生成对应的检查对象 check := &api.AgentServiceCheck{HTTP: fmt.Sprintf("http://%s:%d/health", address, port),Timeout: "10s",Interval: "30s",DeregisterCriticalServiceAfter: "10s", }
// 当然,要在路由初始化时定义 /health Router.GET("/health", func(c *gin.Context) {c.JSON(http.StatusOK, gin.H{"code": http.StatusOK,"success": true,"msg": "very healthy",}) })
- 先把配置文件和日志工具集成进来:
- 同样,新建 config/config.go,打上
mapstructure
tag,为映射成 struct 做准备- 加上 consul 的配置(IP:Port)
main.go
调用 initialize 读取配置文件映射成全局 struct(放在 global 下)就可以开用了- 数据库初始化,包括数据库日志配置
zap.S()
日志初始化
- 不同于 client(API)层,这里可以新建 test 目录使用
var userClient proto.UserClient
测试,不必等到 yapi 统一测试 client 端 API,不好定位问题 - 给 grpc 服务配置健康检查,grpc 本身提供了接口,前面的文章也说过
- 实现
Check
和Watch
方法 - 这里只需要加一句
grpc_health_v1.RegisterHealthServer(server, health.NewServer())
,注册一个用于检查的 server - 这个 Server 还是一个 struct,且实现了上述两个方法,grpc/health 这个库帮我们写好了(可以看源码了解是怎么检查的)
- consul 相当于 client 端,注册到 consul 之后,健康检查时就调用这两个方法
- 为啥consul健康检查时会调用注册的grpc的两个方法呢?因为 consul 对 grpc 的健康检查就是这么定义的,从 consul 文档中也能找到
- 实现
- 按照上面的 demo 注册即可,注意 IP 和 port
- GRPC 或 HTTP 都是在
- user-web 使用 consul 进行服务发现(调用)
- 传统做法,就是连上 consul,过滤得到需要的 user_srv,配置文件和相关代码如下:
- 配置文件和 config 的 struct 一致,但这里我们 service 的 host 和 port 不再是从文件获取(不需要),而是拿着 Name/Id 从 consul 发现,并直接拨号
- 问题也很明显,就是代码很长,比业务逻辑都长;怎么办?我们将连接 consul 获取用户服务这部分抽出来,放在 initialize/srv_conn.go,并返回给 global/UserSrvClient 全局使用
- 这样做还有个好处,已经事先创立好了连接,这样后续就不用进行多次 tcp 的握手
- 但也有问题,如何均衡发现的多个服务呢?这个问题在负载均衡部分解决
- 传统做法,就是连上 consul,过滤得到需要的 user_srv,配置文件和相关代码如下:
动态获取端口
- 本地开发环境端口号固定,但线上环境需要自动获取端口号
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 }
- 网络这块后面会研究,需要用 docker 部署,NGINX 转发
负载均衡
- 什么是负载均衡,前面的文章解释过
- 简而言之,用户到网关,网关到 API,API 到 Service之间,都需要负载均衡,为了抗住并发
- 但是 API 和 Service 之间是 grpc 调用,如何做负载均衡呢?
- 负载均衡策略(架构)
- 进程内的负载均衡策略用的比较广泛,自定义 SDK 不是问题,要尽量避免使用第三方插件
- 负载均衡算法(核心)
- 使用轮询法(round_robin)
- grpc 实现负载均衡
- 先到 srv 部分,demo 还是看前面的文章
- 启动多个 Server 测试,需要将 Serve 监听(请求)放在协程,避免阻塞,影响后续终止信号的监听
- 因为负载均衡算法作用在 consul 和 grpc 上,所以不必在一次进程中连续请求,采用多次启动也能查看负载均衡的效果
- 集成到 user-web,使用负载均衡连接 Server 端的服务
import _ "github.com/mbobakov/grpc-consul-resolver"// 使用这个工具:grpc-consul-resolver,服务发现+负载均衡一并实现 func InitSrvConn() {consulInfo := global.ServerConfig.ConsulInfouserConn, err := grpc.Dial(fmt.Sprintf("consul://%s:%d/%s?wait=14s", consulInfo.Host, consulInfo.Port, global.ServerConfig.UserSrvInfo.Name),grpc.WithInsecure(),grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy": "round_robin"}`),)if err != nil {zap.S().Fatal("[InitSrvConn] 连接 【用户服务失败】")}userSrvClient := proto.NewUserClient(userConn)global.UserSrvClient = userSrvClient }
- 负载均衡的代码目前只体现在 user-web 部分,因为它调用 user-srv,服务发现(不是srv的服务注册)+负载均衡一并使用
grpc-consul-resolver
实现;按道理只要请求其他服务都应该配置负载均衡 - 目前 user-srv 启动多个server做算法验证即可
- 注:user-web 也需要注册到 consul,因为后面也会被调用(发现)
配置中心
- 为什么使用配置中心,这部分在前面的文章分析过,Client(API)端和 Server 端都要用(各自启动时拉取配置)
- 再梳理一下流程
- 技术选型:nacos
- 安装:由于nacos是基于Java的,所以选择使用docker安装,避免手动配置Java环境
docker run --name nacos-standalone -e MODE=standalone -e JVM_XMS=512m -e JVM_XMX=512m -e JVM_XMN=256m -p 8848:8848 -d nacos/nacos-server:latest
- 主要关注:配置管理,命名空间
- go nacos,是 go 语言操作 nacos 的 SDK,参考手册使用
- 写一个 go 的 demo,先搭建好上面环境,新建好配置
package mainimport (//"encoding/json""fmt""github.com/nacos-group/nacos-sdk-go/clients""github.com/nacos-group/nacos-sdk-go/common/constant""github.com/nacos-group/nacos-sdk-go/vo"//"shop_srvs/temp/config" )func main() {// nacos的地址sc := []constant.ServerConfig{{IpAddr: "192.168.109.128",Port: 8848,},}// nacos的client地址cc := constant.ClientConfig{NamespaceId: "26195427-54f4-4a0b-8b25-29e7b654686b", // 如果需要支持多namespace,我们可以创建多个client,它们有不同的NamespaceIdTimeoutMs: 5000,NotLoadCacheAtStart: true,LogDir: "tmp/nacos/log", // 日志和缓存目录,要配置CacheDir: "tmp/nacos/cache",LogLevel: "debug",}// 动态配置客户端;到这一步相当于定位到了nacos的一个命名空间里,接下来只需传入ID和GroupconfigClient, err := clients.CreateConfigClient(map[string]interface{}{"serverConfigs": sc,"clientConfig": cc,})// 创建动态配置客户端的另一种方式 (推荐)//configClient, err := clients.NewConfigClient(// vo.NacosClientParam{// ClientConfig: &clientConfig,// ServerConfigs: serverConfigs,// },//)if err != nil {panic(err)}// 命名空间,group,ID 唯一定位content, err := configClient.GetConfig(vo.ConfigParam{DataId: "user-web",Group: "dev"})if err != nil {panic(err)}fmt.Println(content) //字符串 - json//serverConfig := config.ServerConfig{}//想要将一个json字符串转换成struct,需要去设置这个struct的tag//json.Unmarshal([]byte(content), &serverConfig)//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)// },//})//time.Sleep(3000 * time.Second) }
- 代码包括了将获取到的配置映射成 struct,如果我们的配置文件是yaml格式,可以换成 json(go内置支持的映射方式)
- 这是目前我们项目 user-web 部分的所有配置字段
package configtype 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 AliSmsConfig struct {ApiKey string `mapstructure:"key" json:"key"`ApiSecrect string `mapstructure:"secrect" json:"secrect"` }type ConsulConfig struct {Host string `mapstructure:"host" json:"host"`Port int `mapstructure:"port" json:"port"` }type RedisConfig struct {Host string `mapstructure:"host" json:"host"`Port int `mapstructure:"port" json:"port"`Expire int `mapstructure:"expire" json:"expire"` }type ServerConfig struct {Name string `mapstructure:"name" json:"name"`Port int `mapstructure:"port" json:"port"`UserSrvInfo UserSrvConfig `mapstructure:"user_srv" json:"user_srv"`JWTInfo JWTConfig `mapstructure:"jwt" json:"jwt"`AliSmsInfo AliSmsConfig `mapstructure:"sms" json:"sms"`RedisInfo RedisConfig `mapstructure:"redis" json:"redis"`ConsulInfo ConsulConfig `mapstructure:"consul" json:"consul"` }
- 集成到 user-web
- 总配置文件 config-dev.yaml
host: '192.168.109.128' port: 8848 namespace: '26195427-54f4-4a0b-8b25-29e7b654686b' user: 'nacos' password: 'nacos' dataid: 'user-web' group: 'dev'
- config/config.go 新增 nacos 的配置项,在初始化 InitConfig 里用 viper 读取并映射成 struct 操作
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"` }
- 总配置文件 config-dev.yaml
- 集成到 user_srv
- 和上面的过程类似
- 如何监听 nacos 的配置文件(总配置)的变化呢?使用 viper
- 如何监听远程配置文件的变化呢?
启动项目
- 环境准备
- MySQL;initialize 里只做了初始化连接数据库,需要先手动创建表?handler 里如何确定表名(model.User作为参数即可吗)?
docker run \ --name mysql \ -d \ -p 3306:3306 \ --restart unless-stopped \ -v /mydata/mysql/log:/var/log/mysql \ -v /mydata/mysql/data:/var/lib/mysql \ -v /mydata/mysql/conf:/etc/mysql \ -e MYSQL_ROOT_PASSWORD=123456 \ mysql:5.7-- 按道理,容器暴露出3306端口远程即可连接,但要检查网络是否通畅 -- https://blog.csdn.net/cljdsc/article/details/115207336-- 手动创建数据库,user-srv 服务使用 create database shop_user_srv default charset utf8 collate utf8_general_ci;
- 手动创建表
func main() {// 手动创建数据库 vshop_user_srvdsn := "root:123456@tcp(192.168.109.128:3306)/shop_user_srv?charset=utf8mb4&parseTime=True&loc=Local"// 设置全局的logger,这个logger在我们执行每个sql语句的时候会打印每一行sqlnewLogger := logger.New(log.New(os.Stdout, "\r\n", log.LstdFlags), // io writerlogger.Config{SlowThreshold: time.Second, // 慢 SQL 阈值LogLevel: logger.Info, // Log levelColorful: true, // 禁用彩色打印},)db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{// 表名NamingStrategy: schema.NamingStrategy{// 这里直接使用 model 名SingularTable: true,},Logger: newLogger,})if err != nil {fmt.Println(err)}//定义一个表结构, 将表结构直接生成对应的表 - migrations// 迁移 schema_ = db.AutoMigrate(&model.User{})// 写入一些数据options := &password.Options{16, 100, 32, sha512.New}salt, encodedPwd := password.Encode("admin123", options)newPassword := fmt.Sprintf("$pbkdf2-sha512$%s$%s", salt, encodedPwd)fmt.Println(newPassword)for i := 0; i < 10; i++ {user := model.User{NickName: fmt.Sprintf("Roy%d", i),Mobile: fmt.Sprintf("1878222222%d", i),Password: newPassword,}db.Save(&user)} }
- nacos 上定义配置;本地配置文件定义 nacos 地址及需要的配置集信息即可
// user-srv(dev) {"name": "user-srv","host": "192.168.0.102","port": 64924, // 用于开发环境"tags": ["Roy", "shop", "go"],"mysql": {"db": "shop_user_srv","host": "192.168.109.128","port": 3306,"user": "root","password": "123456"},"consul": {"host": "192.168.109.128","port": 8500},"env": "SHOP_DEBUG" }
// user-web(dev) {"name": "user-web","host": "192.168.0.102", // 在我的Windows上启动"tags":["shop", "Roy", "user", "web"],"port": 8021,// 服务发现"user_srv": {// 只需保留name"name": "user-srv"},"jwt": {"key": "5$!UEmvB#nRB@Iwab#Sy!zofKEOGLRtE"},"sms": {"key": "","secrect": "","expire": 300},"redis": {"host": "192.168.109.128","port": 6379},"consul": {"host": "192.168.109.128","port": 8500},"env": "SHOP_DEBUG" }
- sms 相关的 key 登录自己的阿里云控制台获取
- redis 安装,默认在 6379 端口
- 新建环境变量:SHOP_DEBUG=true,重启 GoLand
- 开发环境端口号固定,生产环境随机获取
- consul 配置
- 项目启动会自动注册的
- 我这里项目启动在 Windows 机器(0.102)
- user-web 和 user-srv 项目都通过配置中心获取 ip/port,定义 ip 主要为了 consul 注册,定义端口主要是为了开发环境使用
- 因为注册在 consul 的端口要看是
dev/pro
,生产环境中是动态的,和这里配置的就不一样了 - 目前,新加service或者client服务器就要新加一份配置(涉及到后期的集群管理和容灾)
- MySQL、consul、nacos、redis、yapi 均启动在虚拟机(109.128)
- 最终上线都要放在 Linux 服务器启动,目前为了方便写代码采用这种方式
- MySQL;initialize 里只做了初始化连接数据库,需要先手动创建表?handler 里如何确定表名(model.User作为参数即可吗)?
- yapi 测试
- 这里是测试 API(user-web)部分
- 要按照这个配置初始化,部署成功后访问:http://ip:3000/,邮箱登录,密码:
ymfe.org
;yapi 底层用了 MongoDB,nacos 底层用了 MySQL
- 导入定义了测试接口的 json 文件;安装扩展(解决跨域问题)
- 设置请求参数并发送
- yapi 可以严格规定请求参数和返回数据的格式,也就是预览中的那份文档,前后端必须严格遵从;可以mock,可以指定值
- 对于前端,要严格传参带header,yapi相当于后端;对于后端,一定要返回指定格式(类型就是数字字符串,但格式很多)的数据,yapi相当于前端
- mock 地址固定,不能换 IP,因为是作为服务器的;发起请求可以换 IP,请求谁都可以
- Q&A
- user-web 中能监听配置文件变化,但好像没起作用?
- 如何监听 nacos 中心的配置变化呢?
- 换了热点,服务端 IP 变了,注册到 consul 异常?
小结
- 这几篇和《Python+Go实践》的内容基本一致,只是 Server 层用了 Go 语言实现而非 Python
- 对一些细节和流程也做了补充强调,有助于理解(也显得废话较多);基本定下了开发基调,后续会快速开发完所有的微服务,实现电商系统
- 最后将重点放在 k8s 部署、单元测试、重构、调优
相关文章:

Go项目(三)
文章目录用户微服务表结构查表web 服务跨域问题图形验证码短信用户注册服务中心注册 grpc 服务动态获取端口负载均衡配置中心启动项目小结用户微服务 作为系统的第一个微服务,开发的技术点前面已经了解了一遍,虽有待补充,但急需实战这里主要…...

CTK学习:(一)编译CTK
CTK插件框架简介 CTK Plugin Framework是用于C++的动态组件系统,以OSGi规范为模型。在此框架下,应用程序由不同的组件组成,遵循面向服务的方法。 ctk是一个开源项目,Github 地址:https://github.com/commontk。 源码地址commontk/CTK: A set of common support code for…...

15种NLP数据增强方法总结与对比
数据增强的方法 数据增强(Data Augmentation,简称DA),是指根据现有数据,合成新数据的一类方法。毕竟数据才是真正的效果天花板,有了更多数据后可以提升效果、增强模型泛化能力、提高鲁棒性等。然而由于NLP…...

Python每日一练(20230219)
目录 1. 循环随机取数组直到得出指定数字? 2. 旋转链表 3. 区间和的个数 1. 循环随机取数组直到得出指定数字? 举个例子: 随机数字范围:0~100 每组数字量:6(s1,s2,s3,s4,s5,s6) 第二轮开始随…...

vTESTstudio - VT System CAPL Functions - VT7001
vtsSerialClose - 关闭VT系统通道的串行端口功能:关闭由系统变量命名空间指定的VT系统通道的串行端口。Target:目标通道变量空间名称,例如:VTS::ECUPowerSupply返回值:0:成功重置目标通道最大和最小值-1&am…...

「可信计算」论文初步解读
可信计算组织(Ttrusted Computing Group,TCG)是一个非盈利的工业标准组织,它的宗旨是加强在相异计算机平台上的计算环境的安全性。TCG于2003年春成立,并采纳了由可信计算平台联盟(the Trusted Computing Platform Alli…...
CSDN 算法技能树 蓝桥杯-基础 刷题+思考总结
切面条-蓝桥杯-基础-CSDN算法技能树https://edu.csdn.net/skill/algorithm/algorithm-530255df51be437b967cbc4524fe66ea?category188 目录 切面条 大衍数列 门牌制作 方阵转置 微生物增殖 成绩统计 星系炸弹 判断闰年的依据: 特别数的和 *日志统计*(双指…...

信小程序点击按钮绘制定制转发分享图
1. 说明 先上代码片断分享链接: https://developers.weixin.qq.com/s/vl3ws9mA72GG 使用 painter 画图 按钮传递定制化信息 效果如下: 2. 关键代码说明 文件列表如下: {"usingComponents": {"painter": "/com…...

Python自动化测试-使用Pandas来高效处理测试数据
Python自动化测试-使用Pandas来高效处理测试数据 目录:导读 一、思考 二、使用pandas来操作Excel文件 三、使用pandas来操作csv文件 四、总结 一、思考 1.Pandas是什么? 功能极其强大的数据分析库可以高效地操作各种数据集 csv格式的文件Excel文件H…...

语音增强学习路线图Roadmap
语音增强算是比较难的研究领域,从入门到精通有很多台阶,本文介绍一些有价值的书籍,值得反复阅读。主要分为基础类和进阶类书籍,大多都是理论和实践相结合的书籍,编程实践是抓手,让知识和基础理论变扎实。基础书籍《信号…...

nginx配置ssl实现https访问
文章目录一、介绍二、创建证书1、OpenSSL创建自签名密钥和证书三、nginx配置四、开放端口一、介绍 nginx配置ssl证书,实现https访问,可以使用自签名SSL证书或者购买机构颁发的证书两种方式参考链接 https://blog.csdn.net/weixin_39198406/article/deta…...

JavaScript 语句
JavaScript 语句向浏览器发出的命令。语句的作用是告诉浏览器该做什么。JavaScript 语句JavaScript 语句是发给浏览器的命令。这些命令的作用是告诉浏览器要做的事情。下面的 JavaScript 语句向 id"demo" 的 HTML 元素输出文本 "Hello Dolly" :…...

将古老的ASP项目转换为PHP初探
ASP 是一种服务器端脚本语言,主要用于开发动态 Web 应用程序。ASP 可以在服务器上执行代码,并将结果返回给客户端浏览器,实现动态生成 Web 页面的功能。ASP 代码通常包含在 <% %> 标记中,以下是一个简单的 ASP 程序示例&…...

数据结构复习(七)模板类封装实现不带头结点的单链表
一、代码 二、总结 一、代码 #include<iostream> using namespace std;template<class T> struct ListNode {T _data;ListNode* next;ListNode(const T& data T()){_data data;next nullptr;}~ListNode(){next nullptr;} };template<class T> class…...

IDEA插件 RestfulTool插件——Restful服务开发辅助工具集
IDEA插件 RestfulTool插件——Restful服务开发辅助工具集 目录IDEA插件 RestfulTool插件——Restful服务开发辅助工具集1.插件介绍2.安装方式3.使用方法1.插件介绍 RestfulTool插件。一套 Restful 服务开发辅助工具集: 提供了一个 Services tree 的显示窗口 双击 …...

2023年全国最新会计专业技术资格精选真题及答案1
百分百题库提供会计专业技术资格考试试题、会计考试预测题、会计专业技术资格考试真题、会计证考试题库等,提供在线做题刷题,在线模拟考试,助你考试轻松过关。 11.下列各项中,影响企业利润表“利润总额”项目的是(&…...

Linux 配置RAID组
目录 配置RAID(软件RAID) 创建RAID组 RAID中出现坏盘如何操作 RAID 添加热备盘 删除RAID组 RAID所解决的问题 提升硬盘的I/O吞吐率 提高硬盘的读写能力 提高硬盘的安全性 进行备份 减少硬盘成本 RAID级别 存储RAID——RAID级别_静下心来敲木鱼的博…...

【2021/推荐/社交网络】Socially-Aware Self-Supervised Tri-Training for Recommendation
部分公式、图表和排版等显示可能异常,可在个人公众号(码农的科研笔记)进行全文免费阅读。 【2021/推荐/社交网络】Socially-Aware Self-Supervised Tri-Training for Recommendation 原文:https://dl.acm.org/doi/10.1145/3447548.3467340 源码:[伯乐 SEPT]、https://git…...

Django搭建个人博客Blog-Day06
展示所有文章Django提供的分页功能说明import os os.environ.setdefault(DJANGO_SETTINGS_MODULE, blog.settings.dev) import django django.setup() # 这个时候才有django的环境 所以导入django中的模块必须写在这句话的后面才有效 from articles.models import Articles #…...

DQL 多表查询
1、多表关系 一对多(多对一) 案例: 部门 与 员工的关系 关系: 一个部门对应多个员工,一个员工对应一个部门 实现: 在从表的一方建立外键,指向主表一方的主键 多对多 案例: 学生 与 课程的关系 关系: 一个学生可以选修多门课程&am…...

BUUCTF Reverse xor
题目:BUUCTF Reverse xor 一些犯傻后学到了新东西的记录 查壳,没壳,IDA打开 main函数很好理解,输入一个长度为33的字符串,1-32位与前一位异或后与global相等,则判定flag正确 找global 在strings window直…...

vite和esbuild/roolup的优缺点
esbuild 优点 基于go语言,go是纯机器码不使用 AST,优化了构建流程多线程并行 缺点 esbuild 没有提供 AST 的操作能力。所以一些通过 AST 处理代码的 babel-plugin 没有很好的方法过渡到 esbuild 中(比如babel-plugin-import)。…...

32-Golang中的map
Golang中的map基本介绍基本语法map声明的举例map使用的方式map的增删改查操作map的增加和更新map的删除map的查找map的遍历map切片基本介绍map排序map的使用细节基本介绍 map是key-value数据结构,又称为字段或者关联数组。类似其它编程语言的集合,在编程…...

LeetCode-384-打乱数组
1、列表随机 为了能够初始化数组,我们使用nums保存当前的数组,利用orignal保存初始化数组。为了实现等可能随机打乱,考虑到随机数本质上是基于随机数种子的伪随机,我们采用如下的方式实现等可能随机:我们将所有元素压…...

九龙证券|重大利好!期货公司打新再“解绑”:可直接参与首发网下配售!
时隔近7年,期货公司及其财物办理子公司参加首发证券网下询价和配售事务再次“解绑”。 2月17日,为适应全面实行股票发行注册制变革需求,中国证券业协会(以下简称中证协)发布《初次公开发行证券网下出资者办理规矩》&am…...

信号完整性设计规则之串扰最小化
本文内容从《信号完整性与电源完整性分析》整理而来,加入了自己的理解,如有错误,欢迎批评指正。 1. 对于微带线和带状线,保持相邻信号路径的间距至少为线宽的2倍。 减小串扰的一种方式就是增大线间距,使线间距等于线…...

Windows Ubuntu双系统 设置时间同步方式
文章目录0 前言1 系统时间机制1.1 Windows时间机制1.2 Ubuntu时间机制2 设置Ubuntu的时间机制3 参考0 前言 在安装windows与ubuntu的双系统之后,会发现两个系统的时间不一致,如果使用了Ubuntu之后,再使用windows就会发现时间变早。原因是两个…...

【python】英雄联盟电竞观赛引擎 掉落提示 CapsuleFarmerEvolved 「Webhook」「钉钉」
介绍 本项目链接 Github本项目链接 Gitee本项目链接 最近在github上发现一个可以用来自动帮你挂英雄联盟(除国服)电竞引擎(可以开出头像和表情)的项目,CapsuleFarmerEvolved,github原项目链接简单来说就是本来是通过看比赛获取奖励的,它帮助你进行观看. 对这个活动有兴趣的话…...

加油站会员管理小程序实战开发教程11
我们已经用了10篇的篇幅讲解了首页的功能,首页主要是用来展示信息的。那么接下来就要进入我们的功能页面了,会员管理一个比较重要的功能是充值的功能。 要想实现充值功能,首先需要办一张会员卡,那什么时候办理会员卡呢?需要先注册成为会员,然后进行开卡的动作。这里要注…...

Python量化入门:投资的风险有哪些?
在金融资产中,风险是指获得收益的不确定性,通常以实际收益与期望收益的偏离来表示。 影响资产收益的因素有很多,而且不同资产面对的风险也不尽相同,在详细介绍风险度量之前,我们有必要了解一下风险的来源。 资产风险的来源 1. 市场风险 市场风险就是我们常说的系统…...