vblog 微博客项目
需求
想要Markdown来写一本书 gitbook/typora
Markdown博客的需求
产品原型
用户:
- 访客:不用登陆 就能浏览文章, 登录后才能进行评论
- 博客写手: 创作者登录后, 发布文章(Markdown编辑器)
流程: 发布博客, 访客可以在界面搜索并且查看博客
产品原型:
软件架构
业务功能架构架构
产品的研发
后端开发
- 接口设计
- 流程设计
做为一个项目过程, 他有哪些部件构建(项目骨架)
统一采用 轻量级的DDD模式
- 对象与数据
vblog项目初始化
go mod init gitee.com/go-course/go12/vblog
- 编程方式
编写user模块
- Interface定义
// 定义User包的能力 就是接口定义
// 站在使用放的角度来定义的 userSvc.Create(ctx, req), userSvc.DeleteUser(id)
// 接口定义好了,不要试图 随意修改接口, 要保证接口的兼容性
type Service interface {
// 创建用户
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)
}
第一个小版本
如何优雅的解决对象依赖(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)
- 定义博客管理模块的接口
- 实现博客管理模块的接口
- 实现博客管理模块的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))
- 编译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
- 每次都写怎么长的命令, 能不能定义别名或者快捷指令
使用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}'
- 如果为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;
}
- 安装该插件: github.com/favadi/protoc-go-inject-tag
- 把需要补充的标签通过注释 写在对应的注释上面: @gotags: json:"id" validate:"required,lte=64"
- 执行该插件: protoc-go-inject-tag -input=apps//.pb.go
- 补充到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
- 定义页面接口(protobuf)
service RPC {
// comment.Comment, rpc.proto/model.proto 属于同一包: comment
// 同名包下面的message 引入后可以直接使用, 不用添加包名称
rpc CreateComment(CreateCommentRequest) returns (Comment);
}
- 服务类grpc实现
实现Service接口(既然包含对外RPC实现,也要同时实现 内部模块调用的实现)
Impl Controller对象 由多重身份:
- 内部服务的具体实现 (http 接口)
- 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)
}
}
}
- 加载comment 服务, 托管给ioc
_ "gitee.com/go-course/go12/vblog/apps/comment/impl"
- 启动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)
}
}()