vblog

command module
v0.0.0-...-8a8b3aa Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 9, 2024 License: MIT Imports: 13 Imported by: 0

README

vblog 微博客项目

需求

想要Markdown来写一本书 gitbook/typora

Markdown博客的需求

产品原型

用户:

  • 访客:不用登陆 就能浏览文章, 登录后才能进行评论
  • 博客写手: 创作者登录后, 发布文章(Markdown编辑器)

流程: 发布博客, 访客可以在界面搜索并且查看博客

产品原型:

软件架构

  • api server
  • ui

业务功能架构架构

产品的研发

后端开发
  1. 接口设计

  1. 流程设计

做为一个项目过程, 他有哪些部件构建(项目骨架)

统一采用 轻量级的DDD模式

  1. 对象与数据

vblog项目初始化

go mod init gitee.com/go-course/go12/vblog
  1. 编程方式

编写user模块
  1. Interface定义
// 定义User包的能力 就是接口定义
// 站在使用放的角度来定义的   userSvc.Create(ctx, req), userSvc.DeleteUser(id)
// 接口定义好了,不要试图 随意修改接口, 要保证接口的兼容性
type Service interface {
	// 创建用户
	CreateUser(context.Context, *CreateUserRequest) (*User, error)
	// 删除用户
	DeleteUser(context.Context, *DeleteUserRequest) error
}
  1. Interface实现(TDD)
  • 2.1 定义一个对象来实现这个接口
  • 2.2 补充依赖的配置管理
  • 2.3 单元测试如何读取到配置
  • 2.4 补充数据库的表
  • 2.5 程序当中的异常如此处理? 都通过Error返回吗?
编写token模块
  • 2.1 定义一个对象来实现这个接口

  • 2.2 实现接口(TDD)

  • 2.3 Restful接口开发 Restful API(Web Service) 基于Gin框架处理HTTP协议数据

// Login HandleFunc
func (h *TokenApiHandler) Login(c *gin.Context) {
	// 1. 获取用户的请求参数, 参数在Body里面
	// 一定要使用JSON
	req := token.NewLoginRequest()

	// json.unmarsal
	// http boyd ---> LoginRequest Object
	err := c.BindJSON(req)
	if err != nil {
		c.JSON(http.StatusBadRequest, err.Error())
		return
	}

	// 2. 执行逻辑
	// 把http 协议的请求 ---> 控制器的请求
	ins, err := h.svc.Login(c.Request.Context(), req)
	if err != nil {
		c.JSON(http.StatusBadRequest, err.Error())
		return
	}

	// 3. 返回响应
	c.JSON(http.StatusOK, ins)
}
// 需要把HandleFunc 添加到Root路由,定义 API ---> HandleFunc
// 可以选择把这个Handler上的HandleFunc都注册到路由上面
func (h *TokenApiHandler) Registry(r gin.IRouter) {
	// r 是Gin的路由器
	r.POST("/tokens/", h.Login)
	r.DELETE("/tokens/", h.Logout)
}
程序入口

把我们控制器和对象组织启动, 启动程序

接口的数据格式统计
// 正常请求数据返回
func Success(c *gin.Context, data any) {
	c.JSON(http.StatusOK, data)
}

// 异常情况的数据返回, 返回我们的业务Exception
func Failed(c *gin.Context, err error) {
	var e *exception.ApiException
	if v, ok := err.(*exception.ApiException); ok {
		e = v
	} else {
		// 非可以预期, 没有定义业务的情况
		e = exception.New(http.StatusInternalServerError, err.Error())
		e.HttpCode = http.StatusInternalServerError
	}

	c.JSON(e.HttpCode, e)
}

第一个小版本

如何优雅的解决对象依赖(ioc)

//2. 初始化控制
// 2.1 user controller
userServiceImpl := userImpl.NewUserServiceImpl()

// 2.2 token controller
tokenServiceImpl := tokenImpl.NewTokenServiceImpl(userServiceImpl)

// 2.3 token api handler
tkApiHandler := tokenApiHandler.NewTokenApiHandler(tokenServiceImpl)

// ...

开发博客业务模块(blog)
  1. 定义博客管理模块的接口
  2. 实现博客管理模块的接口
  3. 实现博客管理模块的HTTP API
  4. 加载业务实现对象和HTTP API对象
关于 Api服务的认证(中间件)

认证流程

中间件写在那个包里面

// 怎么鉴权?
// Gin中间件 func(*Context)
func (a *TokenAuther) Auth(c *gin.Context) {
	// 1. 获取Token
	at, err := c.Cookie(token.TOKEN_COOKIE_NAME)
	if err != nil {
		if err == http.ErrNoCookie {
			response.Failed(c, token.CookieNotFound)
			return
		}
		response.Failed(c, err)
		return
	}

	// 2.调用Token模块来认证
	in := token.NewValiateToken(at)
	tk, err := a.tk.ValiateToken(c.Request.Context(), in)
	if err != nil {
		response.Failed(c, err)
		return
	}

	// 把鉴权后的 结果: tk, 放到请求的上下文, 方便后面的业务逻辑使用
	if c.Keys == nil {
		c.Keys = map[string]any{}
	}
	c.Keys[token.TOKEN_GIN_KEY_NAME] = tk
}

中间件怎么用

// 后台管理接口 需要认证
v1.Use(middlewares.NewTokenAuther().Auth)

使用请求上下文

// 从Gin请求上下文中: c.Keys, 获取认证过后的鉴权结果
tkObj := c.Keys[token.TOKEN_GIN_KEY_NAME]
tk := tkObj.(*token.Token)
关于鉴权(扩展内容)

编写鉴权中间件

// 写带参数的 Gin中间件
func Required(r user.Role) gin.HandlerFunc {
	a := NewTokenAuther()
	a.role = r
	return a.Perm
}

// 权限鉴定, 鉴权是在用户已经认证的情况之下进行的
// 判断当前用户的角色
func (a *TokenAuther) Perm(c *gin.Context) {
	tkObj := c.Keys[token.TOKEN_GIN_KEY_NAME]
	if tkObj == nil {
		response.Failed(c, exception.NewPermissionDeny("token not found"))
		return
	}

	tk, ok := tkObj.(*token.Token)
	if !ok {
		response.Failed(c, exception.NewPermissionDeny("token not an *token.Token"))
		return
	}

	fmt.Printf("user %s role %d \n", tk.UserName, tk.Role)

	// 如果是Admin则直接放行
	if tk.Role == user.ROLE_ADMIN {
		return
	}

	// 判断角色和要求的角色是否相等
	if tk.Role != a.role {
		response.Failed(c, exception.NewPermissionDeny("role %d not allow", tk.Role))
		return
	}
}

添加中间件Use: middleware.Required(user.ROLE_AUTHOR), h.UpdateBlog, 有先后顺序

	// PUT /vblog/api/v1/blogs/43
	v1.PUT("/:id", middleware.Required(user.ROLE_AUTHOR), h.UpdateBlog)
	// PATCH /vblog/api/v1/blogs/43
	v1.PATCH("/:id", middleware.Required(user.ROLE_AUTHOR), h.PatchBlog)
	// DELETE /vblog/api/v1/blogs/43
	v1.DELETE("/:id", middleware.Required(user.ROLE_AUTHOR), h.DeleteBlog)

修复了多个中间件Aboard的问题:

// 异常情况的数据返回, 返回我们的业务Exception
func Failed(c *gin.Context, err error) {
	// 如果出现多个Handler, 需要通过手动abord
	defer c.Abort()

	var e *exception.ApiException
	if v, ok := err.(*exception.ApiException); ok {
		e = v
	} else {
		// 非可以预期, 没有定义业务的情况
		e = exception.New(http.StatusInternalServerError, err.Error())
		e.HttpCode = http.StatusInternalServerError
	}

	c.JSON(e.HttpCode, e)
}
关于数据权限(访问范围)

他控制数据的访问, A/B 都是作者, 都可以更新博客

一个接口管理者 一种资源, 不能通过接口权限来控制, 只能控制 访问数据的访问

  1. 补充scope 数据访问范围定义:
// 控制用户访问数据的访问
// 操作数据的时候, 加上一个where条件
// 比如用户A10, 要去编辑用户B(12)的文章,  id=10 and create_by = 10
type Scope struct {
	UserId string `json:"user_id"`
}
  1. service impl, 需要适配
exec := i.db.
	WithContext(ctx).
	Where("id = ?", ins.Id)

if scope != nil {
	if scope.UserId != "" {
		exec = exec.Where("create_by = ?", scope.UserId)
	}
}
  1. api层需要控制下 用户scope (controller层不涉及权限控制)
// 优化这部分逻辑
tkObj := c.Keys[token.TOKEN_GIN_KEY_NAME]
if tkObj == nil {
	response.Failed(c, exception.NewPermissionDeny("token not found"))
	return
}

tk, ok := tkObj.(*token.Token)
if !ok {
	response.Failed(c, exception.NewPermissionDeny("token not an *token.Token"))
	return
}

in.Scope = &common.Scope{
	UserId: fmt.Sprintf("%d", tk.UserId),
}
  • 业务实现层 不涉及权限
  • 在 api层控制权限
工程的优化-程序的优雅关闭

s.server.Shutdown(ctx)
关于大前端
  • Web(Js/Html/Css/Js框架)
  • PC(Os平台,Windows UI/Qt, Mac Swift Swift UI Kit), Flutter(Dart), MAUI(C#), Compoents(Kolicon), Web(Brower + Web技术)
  • App(Andriod(Java/kolion), Ios(OC, Swift))
补充由Grpc功能的Comment模块
  1. 编译protobuf
# apps/*/pb/*.proto 编译所有模块下面的所有的protobuf文件 通配
protoc -I=. --go_out=. --go_opt=module="gitee.com/go-course/go12/vblog" --go-grpc_out=. --go-grpc_opt=module="gitee.com/go-course/go12/vblog" apps/*/pb/*.proto common/meta.proto
  1. 每次都写怎么长的命令, 能不能定义别名或者快捷指令

使用Makefile来定义一个 make命令

# 执行上面的 protoc 的命令
make gen 
PKG := "gitee.com/go-course/go12/vblog"

dep: ## Get the dependencies
	@go mod tidy

run: ## Run Server
	@go run main.go

install: ## Install depence go package
	@go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
	@go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
	@go install github.com/favadi/protoc-go-inject-tag@latest

gen: ## protobuf 编译
	@protoc -I=. --go_out=. --go_opt=module=${PKG} --go-grpc_out=. --go-grpc_opt=module=${PKG} apps/*/pb/*.proto common/meta.proto

help: ## Display this help screen
	@grep -h -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
  1. 如果为protobuf 补充自定义标签
// DeleteRoleRequest role删除
// github.com/favadi/protoc-go-inject-tag 会读取 @gotags Struct注释的 @gotags
message DeleteRoleRequest {
    // @gotags: json:"id" validate:"required,lte=64"
    string id = 1;
    // @gotags: json:"delete_policy"
    bool delete_policy = 2;
}
  1. 安装该插件: github.com/favadi/protoc-go-inject-tag
  2. 把需要补充的标签通过注释 写在对应的注释上面: @gotags: json:"id" validate:"required,lte=64"
  3. 执行该插件: protoc-go-inject-tag -input=apps//.pb.go
  4. 补充到makefile中去
gen: ## protobuf 编译
	@protoc -I=. --go_out=. --go_opt=module=${PKG} --go-grpc_out=. --go-grpc_opt=module=${PKG} apps/*/pb/*.proto common/meta.proto
	@protoc-go-inject-tag -input=apps/*/*.pb.go -input=common/meta.pb.go
  1. 定义页面接口(protobuf)
service RPC {
    // comment.Comment, rpc.proto/model.proto 属于同一包: comment
    // 同名包下面的message 引入后可以直接使用, 不用添加包名称
    rpc CreateComment(CreateCommentRequest) returns (Comment);
}
  1. 服务类grpc实现

实现Service接口(既然包含对外RPC实现,也要同时实现 内部模块调用的实现)

Impl Controller对象 由多重身份:

  1. 内部服务的具体实现 (http 接口)
  2. grpc服务的具体实现 (grpc 接口)

如何grpc服务的具体实现 装填到 grpc server里面去, 可以参考(ioc 如何装载 gin http 的api路由的)

有ioc帮忙完成所有grpc对象的统一注册

// 对外暴露 grpc能力 通过gprc server
func (h *CommentServiceImpl) Registry(r *grpc.Server) {
	comment.RegisterRPCServer(r, h)
}
// 所有的对象(Grpc 接口实现) 通过对外通过grpc暴露
func (c *IocContainter) GrpcSerivceRegistry(r *grpc.Server) {
	// 找到被托管的APIHandler
	for _, obj := range c.store {
		if grpcServerImpl, ok := obj.(GrpcHandler); ok {
			grpcServerImpl.Registry(r)
		}
	}
}
  1. 加载comment 服务, 托管给ioc
_ "gitee.com/go-course/go12/vblog/apps/comment/impl"
  1. 启动grpc server main.go
// 启动一个跑Grpc Server的Goroutine
grpcServer := grpc.NewServer()
// 把controller内 所有实现了grpc服务的模块注册给grpc server
ioc.Controller().GrpcSerivceRegistry(grpcServer)

go func() {
	lis, err := net.Listen("tcp", ":1234")
	if err != nil {
		log.Fatal(err)
	}
	err = grpcServer.Serve(lis)
	if err != nil {
		fmt.Printf("start grpc server error, %s", err)
	}
}()

Documentation

The Go Gopher

There is no documentation for this package.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL