go17/vblog/README.md
2025-01-19 18:16:29 +08:00

550 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Web全栈开发(Vblog)
## 软件设计
### 需求
管理markdown个文字的一个网站作者后台发布文章访客前台浏览查看文章
### 流程
![](./docs/flow.drawio)
### 产品原型
https://gitee.com/infraboard/go-course/blob/master/new.md#%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1
![](./docs/page.png)
### 架构(BS)和概要设计
![](./docs/arch.png)
### 业务的详细设计
直接使用Go的接口 来定义业务
```go
// 业务域
type Service interface {
UserService
InnterService
}
// 1. 外部
type UserService interface {
// 颁发令牌 登录
IssueToken(context.Context, *IssueTokenRequest) (*Token, error)
// 撤销令牌 退出
RevolkToken(context.Context, *RevolkTokenRequest) (*Token, error)
}
type RevolkTokenRequest struct {
// 访问令牌
AccessToken string `json:"access_token"`
// 刷新令牌, 构成一对避免AccessToken 泄露,用户可以直接 revolk
RefreshToken string `json:"refresh_token"`
}
type IssueTokenRequest struct {
Username string `json:"username"`
Password string `json:"password"`
// 记住我, Token可能1天过期, 过去时间调整为7天
RememberMe bool `json:"remember_me"`
}
// 内部
type InnterService interface {
// 令牌校验
ValidateToken(context.Context, *ValidateTokenRequest) (*Token, error)
}
type ValidateTokenRequest struct {
// 访问令牌
AccessToken string `json:"access_token"`
}
```
数据库的设计伴随接口设计已经完成
1. 如何基于Vscode 构造单元测试的配置
```json
{
"go.testEnvFile": "${workspaceFolder}/etc/test.env",
}
```
添加工作目录环境变量
```
WORKSPACE_DIR="/Users/xxxx/Projects/go-course/go17/vblog"
```
### 业务模块的实现
TDD (Test Drive Development)
+ 用户模块
```go
// 我要测试的对象是什么?, 这个服务的具体实现
// Service的具体实现现在还没实现
// $2a$10$yHVSVuyIpTrQxwiuZUwSMuaJFsnd4YBd6hgA.31xNzuyTu4voD/QW
// $2a$10$fe0lsMhM15i4cjHmWudroOOIIBR27Nb7vwrigwK.9PhWdFld44Yze
// $2a$10$RoR0qK37vfc7pddPV0mpU.nN15Lv8745A40MkCJLe47Q00Ag83Qru
// https://gitee.com/infraboard/go-course/blob/master/day09/go-hash.md#bcrypt
func TestRegistry(t *testing.T) {
req := user.NewRegistryRequest()
req.Username = "test02"
req.Password = "123456"
ins, err := impl.UserService.Registry(ctx, req)
if err != nil {
t.Fatal(err)
}
t.Log(ins)
}
func TestDescribeUser(t *testing.T) {
ins, err := impl.UserService.DescribeUser(ctx, &user.DescribeUserRequest{
user.DESCRIBE_BY_USERNAME, "admin",
})
if err != nil {
t.Fatal(err)
}
//
// if ins.Password = in.Password
t.Log(ins.CheckPassword("1234567"))
}
```
```go
var UserService user.Service = &UserServiceImpl{}
// 定义一个struct, 用于实现 UserService就是刚才定义的接口
// 怎么才能判断这个结构体没有实现这个接口
type UserServiceImpl struct {
}
// DescribeUser implements user.Service.
func (u *UserServiceImpl) DescribeUser(ctx context.Context, in *user.DescribeUserRequest) (*user.User, error) {
query := datasource.DBFromCtx(ctx)
switch in.DescribeBy {
case user.DESCRIBE_BY_ID:
query = query.Where("id = ?", in.Value)
case user.DESCRIBE_BY_USERNAME:
query = query.Where("username = ?", in.Value)
}
ins := &user.User{}
if err := query.Take(ins).Error; err != nil {
return nil, err
}
return ins, nil
}
// Registry implements user.Service.
func (u *UserServiceImpl) Registry(ctx context.Context, in *user.RegistryRequest) (*user.User, error) {
ins, err := user.New(in)
if err != nil {
return nil, err
}
// 明文密码保持到数据库,是不安全
// 对称加密/非对称, 解密
// 消息摘要, 无法还原
// 怎么知道用户的密码 比对hash 123 -> (xxx)
// md5 sha1/256/512, hmac, ...
// 结果固定
hashPass, err := bcrypt.GenerateFromPassword([]byte(in.Password), bcrypt.DefaultCost)
if err != nil {
return nil, err
}
ins.Password = string(hashPass)
if err := datasource.DBFromCtx(ctx).Create(ins).Error; err != nil {
return nil, err
}
// context.WithValue()
// 无事务的模式
// datasource.DB().Transaction(func(tx *gorm.DB) error {
// ctx := datasource.WithTransactionCtx(ctx)
// // 1.
// svcA.Call(ctx)
// // 2.
// svcB.Call(ctx)
// // 3.
// svcC.Call(ctx)
// })
return ins, nil
}
```
### API接口层
Gin
```go
package api
import (
"github.com/gin-gonic/gin"
"github.com/infraboard/mcube/v2/http/gin/response"
"gitlab.com/go-course-project/go17/vblog/apps/token"
"gitlab.com/go-course-project/go17/vblog/apps/token/impl"
)
func NewTokenApiHandler() *TokenApiHandler {
return &TokenApiHandler{
token: impl.TokenService,
}
}
type TokenApiHandler struct {
// 业务控制器
token token.UserService
}
// 提供注册功能, 提供一个Group
// book := server.Group("/api/tokens")
func (h *TokenApiHandler) Registry(r *gin.Engine) {
router := r.Group("/api/tokens")
router.POST("/issue", h.IssueToken)
router.POST("/revolk", h.RevolkToken)
}
func (h *TokenApiHandler) IssueToken(ctx *gin.Context) {
in := token.NewIssueTokenRequest("", "")
if err := ctx.BindJSON(in); err != nil {
response.Failed(ctx, err)
return
}
ins, err := h.token.IssueToken(ctx.Request.Context(), in)
if err != nil {
response.Failed(ctx, err)
return
}
response.Success(ctx, ins)
}
func (h *TokenApiHandler) RevolkToken(ctx *gin.Context) {
in := &token.RevolkTokenRequest{}
if err := ctx.BindJSON(in); err != nil {
response.Failed(ctx, err)
return
}
ins, err := h.token.RevolkToken(ctx, in)
if err != nil {
response.Failed(ctx, err)
return
}
response.Success(ctx, ins)
}
```
### 组装程序(v1)
怎么做开发显得专业
```go
package main
import (
"log"
"github.com/gin-gonic/gin"
"github.com/infraboard/mcube/v2/ioc/config/http"
blogApi "gitlab.com/go-course-project/go17/vblog/apps/blog/api"
tokenApi "gitlab.com/go-course-project/go17/vblog/apps/token/api"
"gitlab.com/go-course-project/go17/vblog/config"
)
func main() {
config.LoadConfig()
// gin Engine, 它包装了http server
server := gin.Default()
// 注册业务模块的路有
tokenApi.NewTokenApiHandler().Registry(server)
blogApi.NewBlogApiHandler().Registry(server)
// 服务器启动
if err := server.Run(http.Get().Addr()); err != nil {
log.Println(err)
}
}
```
功能分层架构(MVC): Book Api
业务分区架构(DDD): Vblog
### 测试验证
```sh
curl --location 'http://127.0.0.1:8080/vblog/api/v1/blogs' \
--header 'Content-Type: application/json' \
--data '{
"title": "POSTMAN测试01",
"author": "will",
"content": "post man 测试",
"summary": "Go全栈项目",
"category": "软件开发"
}'
```
```sh
curl --location 'http://127.0.0.1:8080/vblog/api/v1/tokens' \
--header 'Content-Type: application/json' \
--data '{
"username": "admin",
"password": "123456"
}'
```
### 中间件鉴权
![](./docs/middleware.png)
开发一个认证中间件: 用于根据用户携带的Token信息判断用户身份并把用户身份信息方到上下文中传递给后面HandleFunc中使用
## ioc优化
+ 会用: mcube ioc / golang-ioc
+ 掌握原理: 自己造,手写一个简单
### 问题
+ 手动管理: main, 自己组装对象, 业务越复杂,组装难度越高
![](./docs/oop.png)
+ ioc: 引入了一个中间层, 这个中间层负责对象的管理, 自己对象自己去ioc获取依赖而不是我们开发者 把依赖传递给他,完成 对象的 依赖 由被动 变成主动, ioc, 依赖倒置
![](./docs/ioc.png)
### 基于mcube ioc来改造
![alt text](image.png)
https://www.mcube.top/docs/framework/
1. 对象注册到ioc: 把我们的对象实现了1个IOC对象(符合Ioc接口定义的对象), 可以通过继承基础类直接实现接口ObjectImpl
+ 2 APIHandler: TokenApiHandler, BlogApiHandler
+ 3 Controller: UserServiceImpl, TokenServiceImpl, BlogServiceImpl
对象注册
```go
func init() {
ioc.Controller().Registry(&TokenServiceImpl{})
}
// 定义一个struct, 用于实现 UserService就是刚才定义的接口
// 怎么才能判断这个结构体没有实现这个接口
type TokenServiceImpl struct {
ioc.ObjectImpl
// user service
user user.AdminService
}
func (*TokenServiceImpl) Name() string {
return token.AppName
}
// 他需要自己去获取依赖通过ioc
func (i *TokenServiceImpl) Init() error {
i.user = user.GetService()
return nil
}
```
对象获取
```go
func GetService() Service {
return ioc.Controller().Get(AppName).(Service)
}
```
启动的时候 只需要启动 HTTP Sever就可以啦
```go
func main() {
config.LoadConfig()
// 服务器启动
if err := server.GinServer.Run(http.Get().Addr()); err != nil {
log.Println(err)
}
}
```
修改为使用cli来用
+ cobra 的 OnInitialize 进行ioc配置的读取与加载
+ start 命令 调用 server.run 来进行 服务启动
### ioc 托管Gin框架
```go
// module_name
func (h *BlogApiHandler) Name() string {
return "blogs"
}
// router := r.Group("/vblog/api/v1/blogs")
// ioc_gin.ObjectRouter(h)
// 模块的名称, 会作为路径的一部分比如: /mcube_service/api/v1/hello_module/
// 路径构成规则 <service_name>/<path_prefix>/<service_version>/<module_name>
func (h *BlogApiHandler) Init() error {
h.blog = blog.GetService()
// 在ioc获取gin server *gin.Engine
ioc_gin.RootRouter()
// 获取模块路有: url前缀,
r := ioc_gin.ObjectRouter(h)
r.GET("", h.QueryBlog)
r.Use(middleware.Auth)
r.POST("", h.CreateBlog)
return nil
}
```
```go
func main() {
// load config and run with cobra cli
// 使用ioc的 gin来进行路由加载
cmd.Start()
}
```
### 测试验证
```sh
curl --location 'http://127.0.0.1:8080/api/vblog/v1/blogs' \
--header 'Content-Type: application/json' --header 'Authorization: Bearer eaabbe42-9bc0-4a8e-bd86-c92542d062bb' \
--data '{
"title": "POSTMAN测试01",
"author": "will",
"content": "post man 测试",
"summary": "Go全栈项目",
"category": "软件开发"
}'
```
```sh
curl --location 'http://127.0.0.1:8080/api/vblog/v1/tokens' \
--header 'Content-Type: application/json' \
--data '{
"username": "admin",
"password": "123456"
}'
```
## 其他的优化
### Make
```makefile
PKG := "gitlab.com/go-course-project/go17/vblog"
MOD_DIR := $(shell go env GOPATH)/pkg/mod
PKG_LIST := $(shell go list ${PKG}/... | grep -v /vendor/ | grep -v redis)
GO_FILES := $(shell find . -name '*.go' | grep -v /vendor/ | grep -v _test.go)
.PHONY: all dep lint vet test test-coverage build clean
all: build
dep: ## Get the dependencies
@go mod tidy
lint: ## Lint Golang files
@golint -set_exit_status ${PKG_LIST}
vet: ## Run go vet
@go vet ${PKG_LIST}
run: ## Run Server
@go run main.go start
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}'
```
### Cookie机制
需要手动携带Token, Post, 这会比较安全, 但是有些时候不方便, 如果你想要每次服务端调用 浏览器或者Postman这类工具 可以自动给你携带
颁发Token的时候, 通过客户端(浏览器/Postman), Set-Cookie头
1. 颁发Token是不是要 Set-Cookie
```go
// 设置Cookie
ctx.SetCookie(token.COOKIE_NAME, ins.AccessToken, ins.AccessTokenExpireTTL(), "/", application.Get().Domain(), false, true)
```
2. 获取Token是不是也需要Cookie当作获取Token
```go
// 再尝试从cookie中获取
if accessToken == "" {
tc, err := c.Cookie(token.COOKIE_NAME)
if err != nil {
log.L().Error().Msgf("get cookie error, %s", err)
} else {
accessToken = tc
}
}
```
### 优雅关闭
```go
// 优雅关闭HTTP服务
if err := h.server.Shutdown(ctx); err != nil {
return fmt.Errorf("http graceful shutdown timeout, force exit")
}
```
### DI: 依赖注入
通过配置 ---> 获取一个对象,直接使用
+ 注入工具https://www.mcube.top/docs/framework/component/mysql/
+ 为你程序注入配置:
```toml
[token]
default_expired_ttl = 3600
```
```go
// 定义一个struct, 用于实现 UserService就是刚才定义的接口
// 怎么才能判断这个结构体没有实现这个接口
// unmashal TokenServiceImpl <---> Config文件
type TokenServiceImpl struct {
ioc.ObjectImpl
// user service
user user.AdminService
// 如果你控制器有一些配置
DefaultExpiredTTL int `json:"default_expired_ttl" yaml:"default_expired_ttl" toml:"default_expired_ttl" env:"DEFAULT_EXPIRED_TTL"`
}
func (*TokenServiceImpl) Name() string {
return token.AppName
}
```
## 项目的部署