go17/vblog/README.md
2024-12-15 12:10:11 +08:00

8.5 KiB
Raw Blame History

Web全栈开发(Vblog)

软件设计

需求

管理markdown个文字的一个网站作者后台发布文章访客前台浏览查看文章

流程

产品原型

https://gitee.com/infraboard/go-course/blob/master/new.md#%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1

架构(BS)和概要设计

业务的详细设计

直接使用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 构造单元测试的配置
{
    "go.testEnvFile": "${workspaceFolder}/etc/test.env",
}

添加工作目录环境变量

WORKSPACE_DIR="/Users/xxxx/Projects/go-course/go17/vblog"

业务模块的实现

TDD (Test Drive Development)

  • 用户模块
// 我要测试的对象是什么?, 这个服务的具体实现
// 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"))
}
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

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)

怎么做开发显得专业

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

测试验证

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": "软件开发"
}'
curl --location 'http://127.0.0.1:8080/vblog/api/v1/tokens' \
--header 'Content-Type: application/json' \
--data '{
    "username": "admin",
    "password": "123456"
}'

中间件鉴权

开发一个认证中间件: 用于根据用户携带的Token信息判断用户身份并把用户身份信息方到上下文中传递给后面HandleFunc中使用

ioc优化

  • 会用: mcube ioc / golang-ioc
  • 掌握原理: 自己造,手写一个简单

问题

  • 手动管理: main, 自己组装对象, 业务越复杂,组装难度越高

  • ioc: 引入了一个中间层, 这个中间层负责对象的管理, 自己对象自己去ioc获取依赖而不是我们开发者 把依赖传递给他,完成 对象的 依赖 由被动 变成主动, ioc, 依赖倒置

基于mcube ioc来改造

alt text

https://www.mcube.top/docs/framework/

  1. 对象注册到ioc: 把我们的对象实现了1个IOC对象(符合Ioc接口定义的对象), 可以通过继承基础类直接实现接口ObjectImpl
    • 2 APIHandler: TokenApiHandler, BlogApiHandler
    • 3 Controller: UserServiceImpl, TokenServiceImpl, BlogServiceImpl

对象注册

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
}

对象获取

func GetService() Service {
	return ioc.Controller().Get(AppName).(Service)
}

启动的时候 只需要启动 HTTP Sever就可以啦

func main() {
	config.LoadConfig()

	// 服务器启动
	if err := server.GinServer.Run(http.Get().Addr()); err != nil {
		log.Println(err)
	}
}