vblog

command module
v0.0.0-...-28af436 Latest Latest
Warning

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

Go to latest
Published: Nov 4, 2023 License: MIT Imports: 10 Imported by: 0

README

vblog 微博客项目

需求

想要Markdown来写一本书 gitbook/typora

Markdown博客的需求

产品原型

用户:

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

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

产品原型:

软件架构

  • api server
  • ui

产品的研发

项目后端

  • main.go: 项目入口文件
  • dist: 不是一个包, 存放项目构建文件的目录, vblog-api
  • conf: 项目配置管理对象, 项目配置加载
    • config.go: 负责定义项目配置对象
    • load.go: 用于加载配置对象, toml, env, ....
  • cmd: 项目的CLI接口, vblgo service start -f .../config.toml
    • root.go: CLI的入口命令
  • etc: 不是Go 的package, 用于存储项目配置文件的地方
    • config.toml: toml格式项目配置文件
    • unit_test.env: 测试用例 环境变量配置
  • docs: 存放项目文档
  • protocol: 项目协议处理模块,
    • grpc.go:grpc server, 用于暴露内部Grpc服务(x)
    • http.go: http server, 暴露HTTP接口, 供前端使用(V)
  • apps:
    • blog: 文章管理模块
    • tag: 标签管理模块
    • user: 登录管理模块
      • interface.go: user模块功能接口定义
      • model.go: user模块数据结构定义
      • impl: user模块的服务实现类
        • impl.go: 实现类的定义
        • impl_test.go: 实现类的单元测试
        • user.go: 实现的业务实现逻辑
后端开发
  1. 接口设计

  1. 流程设计

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

统一采用 轻量级的DDD模式

  1. 对象与数据

vblog项目初始化

go mod init gitee.com/he-zw/Go12/vblog

go mod tidy // 下载依赖
  1. 编程方式

编写user模块
  1. Interface定义
// 定义User包的能力 就是接口定义
// 站在使用放的角度来定义的   userSvc.Create(ctx, req), userSvc.DeleteUser(id)
// 接口定义好了,不要试图 随意修改接口, 要保证接口的兼容性
type Service interface { // 定义一个Service的接口
	// 创建用户
	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)
}

第一个小版本

如何优雅的解决对象依赖

//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)

// ...

开发博客业务模块
  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))

Documentation

The Go Gopher

There is no documentation for this package.

Directories

Path Synopsis
user
这是一个user的领域包
这是一个user的领域包

Jump to

Keyboard shortcuts

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