代码拉取完成,页面将自动刷新
博客编写与发布
目标用户:
博客浏览页
CREATE TABLE `blogs` (
`id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '文章的Id',
`tags` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '标签',
`created_at` int NOT NULL COMMENT '创建时间',
`published_at` int NOT NULL COMMENT '发布时间',
`updated_at` int NOT NULL COMMENT '更新时间',
`title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '文章标题',
`author` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '作者',
`content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '文章内容',
`status` tinyint NOT NULL COMMENT '文章状态',
`summary` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '文章概要信息',
`create_by` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT '创建人',
`audit_at` int NOT NULL COMMENT '审核时间',
`is_audit_pass` tinyint NOT NULL COMMENT '是否审核通过',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_title` (`title`) COMMENT 'titile添加唯一键约束'
) ENGINE=InnoDB AUTO_INCREMENT=47 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE `users` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`created_at` int NOT NULL COMMENT '创建时间',
`updated_at` int NOT NULL COMMENT '更新时间',
`username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户名, 用户名不允许重复的',
`password` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT '不能保持用户的明文密码',
`label` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户标签',
`role` tinyint NOT NULL COMMENT '用户的角色',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `idx_user` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE `tokens` (
`created_at` int NOT NULL COMMENT '创建时间',
`updated_at` int NOT NULL COMMENT '更新时间',
`user_id` int NOT NULL COMMENT '用户的Id',
`username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户名, 用户名不允许重复的',
`access_token` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户的访问令牌',
`access_token_expired_at` int NOT NULL COMMENT '令牌过期时间',
`refresh_token` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT '刷新令牌',
`refresh_token_expired_at` int NOT NULL COMMENT '刷新令牌过期时间',
PRIMARY KEY (`access_token`) USING BTREE,
UNIQUE KEY `idx_token` (`access_token`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
注意:
现在流行尽量避免使用外建, 由程序更加业务逻辑 自行决定整个关联关系怎么处理
这张风格如何表现在API?
1. 资源定义
1.1 一类资源
/vblogs/api/v1/blogs: blogs 就是资源的类型: blogs 博客
/vblogs/api/v1/users: users 就是资源的类型: users 用户
1.2 一个资源
/vblogs/api/v1/users/1: 1 就是资源的id, id为1的资源
2. 状态转换: 通过HTTP Method来定义只有的状态转化, 理解为用户的针对某类或者某个资源的动作
POST: 创建一个类型的资源, POST /vblogs/api/v1/users 创建一个用户, 具体的参数存放在body
PATCH: 部分修改(补丁), PATCH /vblogs/api/v1/users/1, 对id为1的用户 做属性的部分修改, name:abc ("usera" ---> "abc")
PUT: 全量修改(覆盖), PUT /vblogs/api/v1/users/1, 对id为1的用户 做属性的全量修改, name:abc 除去name之外的所有属性全部清空
DELETE: 资源删除
GET: 获取一类资源: GET /vblogs/api/v1/users, 获取一个资源 GET /vblogs/api/v1/users/1
其他风格的API
POST url命名动作来表示资源的操作: POST /vblogs/api/v1/users/(list/get/delete/update/...)
POST /pods/poda/logs/watch
{
"title": "",
"author": "",
"content": "",
"summary": ""
}
{
"title": "",
"author": "",
"content": "",
"summary": ""
}
{
"title": "",
"author": "",
"content": "",
"summary": ""
}
body不传数据
body不传数据
{
"username": "",
"password": "",
"remember": true,
}
body不传数据
功能完整, 不做API, 可以直接操作数据库, 也可以通过单元测试
go mod init 'gitee.com/baicaijc/vblog'
业务模块开发遵循如下规则:
表现在目录结构上:
API和Interface的区别
// 面向对象
// user.Service, 设计你这个模块提供的接口
// 接口定义, 一定要考虑兼容性, 接口的参数不能变
type Service interface {
// 用户创建
// CreateUser(username, password, role string, lable map[string]string)
// 设计CreateUserRequest, 可以扩展对象, 而不影响接口的定义
// 1. 这个接口支持取消吗? 要支持取消应该怎么办?
// 2. 这个接口支持Trace, TraceId怎么传递?
// 中间件参数,取消/Trace/... 怎么产生怎么传递
CreateUser(context.Context, *CreateUserRequest) (*User, error)
// 查询用户列表, 对象列表 [{}]
QueryUser(context.Context, *QueryUserRequest) (*UserSet, error)
// 查询用户详情, 通过Id查询,
DescribeUser(context.Context, *DescribeUserRequest) (*User, error)
// 作业:
// 用户修改
// 用户删除
}
业务定义层(对业务的抽象), 由impl模块来完成具体的功能实现
// 实现 user.Service
// 怎么判断这个服务有没有实现这个接口喃?
// &UserServiceImpl{} 是会分配内存, 怎么才能不分配内存
// nil 如何生命 *UserServiceImpl 的 nil
// (*UserServiceImpl)(nil) ---> int8 1 int32(1) (int32)(1)
// nil 就是一个*UserServiceImpl的空指针
var _ user.Service = (*UserServiceImpl)(nil)
// 用户创建
func (i *UserServiceImpl) CreateUser(
ctx context.Context,
in *user.CreateUserRequest) (
*user.User, error) {
return nil, nil
}
// 查询用户列表, 对象列表 [{}]
func (i *UserServiceImpl) QueryUser(
ctx context.Context,
in *user.QueryUserRequest) (
*user.UserSet, error) {
return nil, nil
}
// 查询用户详情, 通过Id查询,
func (i *UserServiceImpl) DescribeUser(
ctx context.Context,
in *user.DescribeUserRequest) (
*user.User, error) {
return nil, nil
}
TDD的思想: 保证代码的质量
// 怎么引入被测试的对象
func TestCreateUser(t *testing.T) {
// 单元测试异常怎么处理
u, err := i.CreateUser(ctx, nil)
// 直接报错中断单元流程并且失败
if err != nil {
t.Fatal(err)
}
// 自己进行期望对比,进行单元测试报错
if u == nil {
t.Fatal("user not created")
}
// 正常打印对象
t.Log(u)
}
// 怎么实现user.Service接口?
// 定义UserServiceImpl来实现接口
type UserServiceImpl struct {
// 依赖了一个数据库操作的链接池对象
db *gorm.DB
}
package conf
import (
"fmt"
"sync"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
// 这里不采用直接暴露变量的方式, 比较好的方式 使用函数
var config *Config
// 这里就可以补充逻辑
func C() *Config {
// sync.Lock
if config == nil {
// 给个默认值
config = &Config{}
}
return config
}
// 程序配置对象, 启动时 会读取配置, 并且为程序提供需要全局变量
// 把配置对象做出全局变量(单列模式)
type Config struct {
MySQL *MySQL
}
// db对象也是一个单列模式
type MySQL struct {
Host string `json:"host" yaml:"host" toml:"host" env:"DATASOURCE_HOST"`
Port int `json:"port" yaml:"port" toml:"port" env:"DATASOURCE_PORT"`
DB string `json:"database" yaml:"database" toml:"database" env:"DATASOURCE_DB"`
Username string `json:"username" yaml:"username" toml:"username" env:"DATASOURCE_USERNAME"`
Password string `json:"password" yaml:"password" toml:"password" env:"DATASOURCE_PASSWORD"`
Debug bool `json:"debug" yaml:"debug" toml:"debug" env:"DATASOURCE_DEBUG"`
// 判断这个私有属性, 来判断是否返回已有的对象
db *gorm.DB
l sync.Mutex
}
// dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
func (m *MySQL) DSN() string {
return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
m.Username,
m.Password,
m.Host,
m.Port,
m.DB,
)
}
// 通过配置就能通过一个DB 实例
func (m *MySQL) GetDB() *gorm.DB {
m.l.Lock()
defer m.l.Unlock()
if m.db == nil {
db, err := gorm.Open(mysql.Open(m.DSN()), &gorm.Config{})
if err != nil {
panic(err)
}
m.db = db
}
return m.db
}
// 配置对象提供全局单列配置
func (c *Config) DB() *gorm.DB {
return c.MySQL.GetDB()
}
func NewUserServiceImpl() *UserServiceImpl {
return &UserServiceImpl{
// 获取全局的DB对象
// 前提: 配置对象准备完成
db: conf.C().DB(),
}
}
使用validator来进行参数的校验 "github.com/go-playground/validator/v10"
package impl_test
import (
"context"
"testing"
"gitlab.com/go-course-project/go13/vblog/apps/user"
"gitlab.com/go-course-project/go13/vblog/apps/user/impl"
)
var (
i user.Service
ctx = context.Background()
)
// 怎么引入被测试的对象
func TestCreateUser(t *testing.T) {
// 使用构造函数创建请求对象
// user.CreateUserRequest{}
req := user.NewCreateUserRequest()
req.Username = "member"
req.Password = "123456"
req.Role = user.ROLE_ADMIN
// 单元测试异常怎么处理
u, err := i.CreateUser(ctx, req)
// 直接报错中断单元流程并且失败
if err != nil {
t.Fatal(err)
}
// 自己进行期望对比,进行单元测试报错
if u == nil {
t.Fatal("user not created")
}
// 正常打印对象
t.Log(u)
}
func TestQueryUser(t *testing.T) {
req := user.NewQueryUserRequest()
ul, err := i.QueryUser(ctx, req)
// 直接报错中断单元流程并且失败
if err != nil {
t.Fatal(err)
}
t.Log(ul)
}
func TestDescribeUser(t *testing.T) {
req := user.NewDescribeUserRequest(6)
ul, err := i.DescribeUser(ctx, req)
if err != nil {
t.Fatal(err)
}
t.Log(ul)
}
func init() {
// 加载被测试对象, i 就是User Service接口的具体实现对象
i = impl.NewUserServiceImpl()
}
问题:
方案:
// $2a$10$1MvkjvWOS0/Rf.cEKKxeie/Y7ADz9XZTq09Wd/bKwX/vUv0kdYJ4.
// $2a$10$IyB.w1NVOrBmZ9WOsT6gEuruaynjse2CNmce9399yUErnufV10DX2
// https://gitee.com/infraboard/go-course/blob/master/day09/go-hash.md#bcrypt
func TestHashPassword(t *testing.T) {
req := user.NewCreateUserRequest()
req.Password = "123456"
req.HashPassword()
t.Log(req.Password)
t.Log(req.CheckPassword("1234561"))
}
// Token Service接口定义
type Service interface {
// 登录: 颁发令牌
IssueToken(context.Context, *IssueTokenRequest) (*Token, error)
// 退出: 撤销令牌
RevokeToken(context.Context, *RevokeTokenRequest) (*Token, error)
// 校验令牌
ValidateToken(context.Context, *ValidateTokenRequest) (*Token, error)
}
// 登录: 颁发令牌
// 依赖User模块来检验 用户的密码是否正确
func (i *TokenServiceImpl) IssueToken(
ctx context.Context,
in *token.IssueTokenRequest) (
*token.Token, error) {
// 1. 确认用户密码是否正确
req := user.NewQueryUserRequest()
req.Username = in.Username
us, err := i.user.QueryUser(ctx, req)
if err != nil {
return nil, err
}
if len(us.Items) == 0 {
return nil, fmt.Errorf("用户名或者密码错误")
}
// 校验密码是否正确
if err := us.Items[0].CheckPassword(in.Password); err != nil {
return nil, err
}
// 2. 正确的请求下 就颁发用户令牌
return nil, nil
}
/*
{
"user_id": "9",
"username": "admin",
"access_token": "cmh62ncbajf1m8ddlpa0",
"access_token_expired_at": 7200,
"refresh_token": "cmh62ncbajf1m8ddlpag",
"refresh_token_expired_at": 28800,
"created_at": 1705140573,
"updated_at": 1705140573,
"role": 1
}
*/
func TestIssueToken(t *testing.T) {
req := token.NewIssueTokenRequest("admin", "123456")
req.RemindMe = true
tk, err := i.IssueToken(ctx, req)
if err != nil {
t.Fatal(err)
}
t.Log(tk)
}
func TestRevokeToken(t *testing.T) {
// cmlu26du48h27s06e9sg
req := token.NewRevokeTokenRequest(
"cmlu26du48h27s06e9sg",
"cmlu26du48h27s06e9t0",
)
tk, err := i.RevokeToken(ctx, req)
if err != nil {
t.Fatal(err)
}
t.Log(tk)
}
// refresh token expired 8666.516636 minutes
/*
{
"user_id": "7",
"username": "jack",
"access_token": "cmjv69du48h4442rkf0g",
"access_token_expired_at": 604800,
"refresh_token": "cmjv69du48h4442rkf10",
"refresh_token_expired_at": 604800,
"created_at": 1705505573,
"updated_at": 1705505573,
"role": 0
}
*/
func TestValidateToken(t *testing.T) {
req := token.NewValidateTokenRequest("cmh63mkbajf1o5uh5cb0")
tk, err := i.ValidateToken(ctx, req)
if err != nil {
t.Fatal(err)
}
t.Log(tk)
}
使用Gin做开发API的接口: 接口的状态管理(Cookie)
// 来实现对外提供 RESTful 接口
type TokenApiHandler struct {
svc token.Service
}
// 如何为Handler添加路径, 如果把路由注册给 Http Server
func (h *TokenApiHandler) Registry() {
// 每个业务模块 都需要往Gin Engine对象注册路由
r := gin.Default()
r.POST("/vblog/api/v1/tokens", h.Login)
r.DELETE("/vblog/api/v1/tokens", h.Logout)
}
// 登录
func (h *TokenApiHandler) Login(ctx *gin.Context) {
}
// 退出
func (h *TokenApiHandler) Logout(ctx *gin.Context) {
}
root path --> /vblog/api/v1
module path --> /vblog/api/v1/tokens
// 如何为Handler添加路径, 如果把路由注册给 Http Server
// 需要一个Root Router: path prefix: /vblog/api/v1
func (h *TokenApiHandler) Registry(rr gin.IRouter) {
// 每个业务模块 都需要往Gin Engine对象注册路由
// r := gin.Default()
// rr := r.Group("/vblog/api/v1")
// 模块路径
// /vblog/api/v1/tokens
mr := rr.Group(token.AppName)
mr.POST("tokens", h.Login)
mr.DELETE("tokens", h.Logout)
}
// Body 必须Json
req := token.NewIssueTokenRequest("", "")
if err := c.BindJSON(req); err != nil {
return
}
// 登录
func (h *TokenApiHandler) Login(c *gin.Context) {
// 1. 解析用户请求
// http 的请求可以放到哪里, 放body, bytes
// io.ReadAll(c.Request.Body)
// defer c.Request.Body.Close()
// json unmarshal json.Unmaral(body, o)
// Body 必须Json
req := token.NewIssueTokenRequest("", "")
if err := c.BindJSON(req); err != nil {
response.Failed(c, err)
return
}
// 2. 业务逻辑处理
tk, err := h.svc.IssueToken(c.Request.Context(), req)
if err != nil {
response.Failed(c, err)
return
}
// 2.1 set cookie
c.SetCookie(
token.TOKEN_COOKIE_KEY,
tk.AccessToken,
tk.AccessTokenExpiredAt,
"/",
conf.C().Application.Domain,
false,
true,
)
// 3. 返回处理的结果
response.Success(c, tk)
}
// 退出
func (h *TokenApiHandler) Logout(c *gin.Context) {
// 1. 解析用户请求
// token为了安全 存放在Cookie获取自定义Header中
accessToken := token.GetAccessTokenFromHttp(c.Request)
req := token.NewRevokeTokenRequest(accessToken, c.Query("refresh_token"))
// 2. 业务逻辑处理
_, err := h.svc.RevokeToken(c.Request.Context(), req)
if err != nil {
response.Failed(c, err)
return
}
// 2.1 删除前端的cookie
c.SetCookie(
token.TOKEN_COOKIE_KEY,
"",
-1,
"/",
conf.C().Application.Domain,
false,
true,
)
// 3. 返回处理的结果
response.Success(c, "退出成功")
}
package main
import (
"github.com/gin-gonic/gin"
"gitlab.com/go-course-project/go13/vblog/apps/token/api"
token_impl "gitlab.com/go-course-project/go13/vblog/apps/token/impl"
user_impl "gitlab.com/go-course-project/go13/vblog/apps/user/impl"
)
func main() {
// user service impl
usvc := user_impl.NewUserServiceImpl()
// token service impl
tsvc := token_impl.NewTokenServiceImpl(usvc)
// api
TokenApiHander := api.NewTokenApiHandler(tsvc)
// Protocol
engine := gin.Default()
rr := engine.Group("/vblog/api/v1")
TokenApiHander.Registry(rr)
// 把Http协议服务器启动起来
if err := engine.Run(":8080"); err != nil {
panic(err)
}
}
PS E:\浏览器下载\mage_course\vblog> go run "e:\浏览器下载\mage_course\vblog\main.go"
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] POST /vblog/api/v1/tokens/ --> gitee.com/baicaijc/vblog/apps/token/api.(*TokenApiHandler).Login-fm (3 handlers)
[GIN-debug] DELETE /vblog/api/v1/tokens/ --> gitee.com/baicaijc/vblog/apps/token/api.(*TokenApiHandler).Logout-fm (3
handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Listening and serving HTTP on :8080
使用 postman进行测试
POST http://127.0.0.1:8080/vblog/api/v1/tokens
body josn格式
{
"username": "jack03",
"password": "123456"
}
{
"user_id": "7",
"username": "jack03",
"access_token": "cmmhhstu48h3c23gdmkg",
"access_token_expired_at": 7200,
"refresh_token": "cmmhhstu48h3c23gdml0",
"refresh_token_expired_at": 28800,
"created_at": 1705842931,
"updated_at": 1705842931,
"role": 0
}
DELETE http://127.0.0.1:8080/vblog/api/v1/tokens?refresh_token=cmmhhstu48h3c23gdml0
"退出成功"
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。