258 lines
5.6 KiB
Markdown
258 lines
5.6 KiB
Markdown
# 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的接口 来定义业务
|
||
```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)
|
||
}
|
||
```
|
||
|
||
### 组装程序
|
||
|
||
怎么做开发显得专业
|
||
|
||
|
||
### 中间件鉴权
|
||
|
||
|
||
### ioc优化
|
||
|
||
|