vblog 微博客项目
需求
想要Markdown来写一本书 gitbook/typora
Markdown博客的需求
产品原型
用户:
- 访客:不用登陆 就能浏览文章, 登录后才能进行评论
- 博客写手: 创作者登录后, 发布文章(Markdown编辑器)
流程: 发布博客, 访客可以在界面搜索并且查看博客
产品原型:
软件架构
产品的研发
项目后端
- main.go: 项目入口文件
- dist: 不是一个包, 存放项目构建文件的目录, vblog-api
- conf: 项目配置管理对象, 项目配置加载
- config.go: 负责定义项目配置对象
- load.go: 用于加载配置对象, toml, env, ....
- cmd: 项目的CLI接口, vblgo service start -f .../config.toml
- 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: 实现的业务实现逻辑
后端开发
- 接口设计
- 流程设计
做为一个项目过程, 他有哪些部件构建(项目骨架)
统一采用 轻量级的DDD模式
- 对象与数据
vblog项目初始化
go mod init gitee.com/he-zw/Go12/vblog
go mod tidy // 下载依赖
- 编程方式
编写user模块
- Interface定义
// 定义User包的能力 就是接口定义
// 站在使用放的角度来定义的 userSvc.Create(ctx, req), userSvc.DeleteUser(id)
// 接口定义好了,不要试图 随意修改接口, 要保证接口的兼容性
type Service interface { // 定义一个Service的接口
// 创建用户
CreateUser(context.Context, *CreateUserRequest) (*User, error)
// 删除用户
DeleteUser(context.Context, *DeleteUserRequest) error
}
- Interface实现(TDD)
- 2.1 定义一个对象来实现这个接口
- 2.2 补充依赖的配置管理
- 2.3 单元测试如何读取到配置
- 2.4 补充数据库的表
- 2.5 程序当中的异常如此处理? 都通过Error返回吗?
编写token模块
// 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)
// ...
开发博客业务模块
- 定义博客管理模块的接口
- 实现博客管理模块的接口
- 实现博客管理模块的HTTP API
- 加载业务实现对象和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 都是作者, 都可以更新博客
一个接口管理者 一种资源, 不能通过接口权限来控制, 只能控制 访问数据的访问
- 补充scope 数据访问范围定义:
// 控制用户访问数据的访问
// 操作数据的时候, 加上一个where条件
// 比如用户A10, 要去编辑用户B(12)的文章, id=10 and create_by = 10
type Scope struct {
UserId string `json:"user_id"`
}
- service impl, 需要适配
exec := i.db.
WithContext(ctx).
Where("id = ?", ins.Id)
if scope != nil {
if scope.UserId != "" {
exec = exec.Where("create_by = ?", scope.UserId)
}
}
- 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),
}
工程的优化-程序的优雅关闭
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))