设计结构和代码优化(一)【主要是gin的signup和login这一部分的相关代码,这里主要就是大概过一下内容还有简单的注释,具体里面的一些包到时候再说】

代码结构和理想中的依赖关系

代码结构和理想中的依赖关系
目录结构解释:
cmd/ :里面主要存放的是main文件,用于运行程序
script/ :里面存放的是相关的脚本文件,例如mysql目录下面存放的就是数据库初始化过程中的创建数据库的脚本文件,正式的项目里面一般不会让我们自己创建数据库,通常就是我们提出建库建表的需求,然后数据库管理员来进行创建。
docker、k8s:这一部分就是我们运行程序所需要的一些中间件的创建
internal/ :这一部分是程序的内部文件,属于是整个项目的主体
着重的聊一下internal这个目录:
1.internal/domain :这个目录里存放的主要是领域对象相关的代码

领域对象(Domain Object)是在领域驱动设计(Domain-Driven Design, DDD)中用来表示业务领域核心概念的对象。它们代表了业务领域的实体和概念,并且通常包含了与业务逻辑紧密相关的状态和行为。领域对象可以分为以下几种类型:
聚合根(Aggregate Root):
聚合根是领域模型中的主要实体,它们是聚合的入口点。聚合是一组相关对象的集合,这些对象在业务规则下作为一个整体进行操作。聚合根负责维护聚合内部的一致性。
领域实体(Domain Entity):
实体具有唯一标识符,即使其属性改变,其身份也不会改变。实体通常包含业务逻辑和规则,以及与业务流程相关的状态。
值对象(Value Object):
值对象没有唯一标识符,它们的相等性基于其属性的值。如果两个值对象的属性值相同,则认为它们是相等的。值对象通常用于描述实体的某些方面,如货币金额、地址等。
除了上述三种主要的领域对象类型之外,还有其他一些常见的对象类型,例如:
视图对象(View Object, VO):
用于展示层,封装特定页面或组件所需的数据。
数据传输对象(Data Transfer Object, DTO):
用于在不同的系统组件之间传输数据,通常在展示层和服务层之间使用。
持久化对象(Persistence Object, PO):
用于数据库交互,通常映射到数据库表的结构。
领域对象的核心在于它们不仅持有数据,还封装了业务逻辑,这与简单的数据访问对象(DAO)或贫血模型(Anemic Domain Model)不同,在贫血模型中,业务逻辑通常被放在服务层,领域对象仅作为数据载体。在领域驱动设计中,领域对象是业务逻辑的核心承载者。

在这个项目里面的domain里面的golang代码主要起到了持久化对象的作用,用于与数据库交互,映射到表结构,具体的代码如下:

package domain

import "time"

type User struct {
	Id       int64
	Email    string
	Password string
	Ctime    time.Time
	Utime    time.Time
}

上面的代码,里面的id,email和password就对应到了数据库上面的几个字段,通常使用。gorm.db 里面的AutoMigrate 方法来初始化数据表,具体数据表的初始化内容在dao目录下面。初始化完成创建出来的表结构如下:
在这里插入图片描述

2.internal/repository

internal/repository/user.go里面的代码主要是数据库的抽象并不直接操作数据库,具体的数据库操作在其下面的dao目录里面,具体的解释如下:

repository目录通常用于封装对数据库或外部服务的数据访问逻辑。这个目录下的代码主要负责与数据存储层交互,如执行CRUD(创建、读取、更新、删除)操作,以及更复杂的查询和事务处理。
repository目录内的结构可能包括:
接口定义:
定义数据访问接口,声明所有可能的操作方法。
实现文件:
对应每个接口的具体实现,通常会有一个或多个实现文件,每个文件对应不同的数据源或不同的数据类型。
数据模型:
虽然数据模型(即ORM映射对象)有时放在单独的models或entities目录下,但在一些项目中,repository目录也可能包含这些模型的引用或直接定义。
单元测试:
每个实现文件通常都有对应的单元测试文件,以确保数据访问逻辑的正确性。

repository/user.go目前的主要代码如下,主要定义了一些查询方法,还有进行数据库的访问,具体的调用逻辑也是完全按照开篇图片里面红色箭头进行调用的,代码如下:

package repository

import (
	"context"

	"git.com/gin_basic/webook/internal/domain"
	"git.com/gin_basic/webook/internal/repository/dao"
)
var (
	ErrDuplicateEmail = dao.ErrDuplicateEmail
	ErrRecordNotFound = dao.ErrRecordNotFound
)

type UserRepository struct {
	dao *dao.UserDAO
}

func (repo *UserRepository) Create(ctx context.Context, u domain.User) error {
	return repo.dao.Insert(ctx, dao.User{
		Email:    u.Email,
		Password: u.Password,
	})
}

func (repo *UserRepository) FindByEmail(ctx context.Context, email string) (domain.User, error) {
	u, err := repo.dao.FindByEmail(ctx, email)
	if err != nil {
		return domain.User{}, err
	}
	return repo.toDomain(u), err

}
func (repo *UserRepository) toDomain(u dao.User) domain.User {
	return domain.User{
		Id:       u.Id,
		Email:    u.Email,
		Password: u.Password,
	}

}

func NewUserRepository(dao *dao.UserDAO) *UserRepository {
	return &UserRepository{
		dao: dao,
	}
}

2.1 internal/repository/cache/

这部分主要是用来存储缓存的,具体的操作还没有用到,后续补充。

2.2 internal/repository/dao/user.go 和init.go

init.go 这部分主要是为了初始化数据表,具体的代码如下:

package dao

import "gorm.io/gorm"

func InitTables(db *gorm.DB) error {
	db.AutoMigrate(&User{})
	return nil
}

user.go 这部分主要是一些数据库的具体操作,包括数据表里面的一些字段的定义,包括主键和唯一索引,数据库查询操作,虽然repository/user.go里面也有一些查询方法,但是里面的方法主要是通过调用dao/user.go 里面的方法来进行实现的。还有数据表里面包含时间的存储格式,还有具体的错误的定义。具体代码如下:

package dao

import (
	"context"
	"errors"
	"time"

	"github.com/go-sql-driver/mysql"
	"gorm.io/gorm"
)

// UserDAO 是用户数据访问对象,负责用户相关数据的操作。
type UserDAO struct {
	db *gorm.DB
}

// ErrDuplicateEmail 表示邮箱重复的错误。
var ErrDuplicateEmail = errors.New("邮箱冲突")

// ErrRecordNotFound 表示记录未找到的错误。
var ErrRecordNotFound = gorm.ErrRecordNotFound

// Insert 插入一个新用户到数据库。
// ctx: 操作的上下文。
// u: 待插入的用户对象。
// 返回错误,如果插入失败或邮箱已存在。
func (dao *UserDAO) Insert(ctx context.Context, u User) error {
	now := time.Now().UnixMilli()
	u.Ctime = now
	u.Utime = now
	err := dao.db.WithContext(ctx).Create(&u).Error
	if me, ok := err.(*mysql.MySQLError); ok {
		const duplicateErr uint16 = 1062
		if me.Number == duplicateErr {
			return ErrDuplicateEmail
		}
	}
	return err
}

// FindByEmail 根据邮箱查找用户。
// ctx: 操作的上下文。
// email: 待查找的邮箱。
// 返回用户对象和错误,如果未找到用户或发生其他错误。
func (dao *UserDAO) FindByEmail(ctx context.Context, email string) (User, error) {
	var u User
	err := dao.db.WithContext(ctx).Where("email=?", email).First(&u).Error
	return u, err
}

// NewUserDAO 创建一个新的UserDAO实例。
// db: GORM数据库连接。
// 返回UserDAO实例。
func NewUserDAO(db *gorm.DB) *UserDAO {
	return &UserDAO{
		db: db,
	}
}

// User 是用户实体。
type User struct {
	Id       int64  `gorm:"primaryKey,autoIncrement"`
	Email    string `gorm:"unique"`
	Password string
	Ctime    int64 `gorm:"autoCreateTime"`
	Utime    int64 `gorm:"autoUpdateTime"`
}

3.internal/repository/service

service 目录,里面存放的是主要的业务逻辑代码,用于一些接口的实现和错误的处理。service目录下的具体内容可以包括如下几部分,具体的代码如下:

service目录通常用于封装业务逻辑。这里的业务逻辑是指那些处理数据和业务规则的代码,它们通常独立于HTTP请求和响应的细节。service目录下的文件和包可以包含以下几种类型的代码:
业务逻辑处理:
这些函数或方法负责执行应用程序的核心功能,例如用户认证、购物车管理、订单处理等。
它们可能调用repository层来获取或更新数据,并且可能包含复杂的业务规则和流程控制。
依赖注入:
service层可能会依赖于其他服务或者组件,如数据库访问层、外部API、缓存系统等。
依赖注入模式可以在这里被采用,以提高代码的可测试性和解耦。
接口实现:
可能会定义一些接口,然后在service目录下实现这些接口,以便于代码的重用和替换。
错误处理:
业务逻辑中可能包含错误处理机制,确保在发生问题时能够优雅地处理并返回适当的错误信息。
事务管理:
如果涉及到多个数据库操作,service层可能是处理事务的地方,确保数据的一致性。
在service目录中的代码通常不直接与HTTP请求交互,而是通过controller层调用。controller层负责接收HTTP请求,解析请求参数,调用service层的相应方法,然后将结果转换为HTTP响应发送回客户端。
这样的设计有助于保持代码的清晰和模块化,使得每个部分都有明确的职责,同时也便于单元测试和维护。

插入1个名词解释:
依赖注入:

依赖注入(Dependency Injection,简称DI)是一种设计模式,用于实现控制反转(Inversion of Control, IoC)。在软件工程中,依赖注入允许我们以松耦合的方式编写代码,使得组件之间的依赖关系可以在运行时动态地配置,而不是在编译时硬编码。
依赖注入有三种主要类型:
构造器注入:
依赖项通过构造函数参数传递给对象。这是最推荐的方式,因为它可以确保依赖项的不可变性和对象的初始化状态。
属性注入:
依赖项通过setter方法或公开字段注入到对象中。这种方式不如构造器注入安全,因为依赖项可能在对象的生命周期中被更改。
方法注入:
依赖项通过特定的方法调用注入。这通常用于注入一些策略或行为,而不是核心依赖。

package service

import (
	"context"
	"errors"

	"git.com/gin_basic/webook/internal/domain"
	"git.com/gin_basic/webook/internal/repository"
	"golang.org/x/crypto/bcrypt"
)

var (
	ErrDuplicateEmail        = repository.ErrDuplicateEmail
	ErrInvalidUserOrPassword = errors.New("用户名称或者密码不对")
)

type UserService struct {
	repo *repository.UserRepository
}

func NewUserService(repo *repository.UserRepository) *UserService {
	return &UserService{
		repo: repo,
	}
}
func (svc *UserService) SignUp(ctx context.Context, u domain.User) error {
	hash, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
	if err != nil {
		return err
	}
	u.Password = string(hash)
	return svc.repo.Create(ctx, u)
}

func (svc *UserService) Login(ctx context.Context, email string, password string) (domain.User, error) {
	u, err := svc.repo.FindByEmail(ctx, email)
	if errors.Is(err, repository.ErrRecordNotFound) {
		return domain.User{}, ErrInvalidUserOrPassword

	}
	if err != nil {
		return domain.User{}, err
	}
	err = bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password))
	if err != nil {
		return domain.User{}, ErrInvalidUserOrPassword
	}
	return u, nil
}

4.internal/repoository/web

web 目录见名知意,主要是用来和http交互的代码,里面定义了路由和访问方式,以及整个代码具体都有哪些实现,主要是组织代码和资源文件,internal的各个目录里面的代码都是基于web里面的定义进行实现的,然后由web代码进行调用。最后实现功能。例如

package web

import (
	"errors"
	"net/http"

	"git.com/gin_basic/webook/internal/domain"
	"git.com/gin_basic/webook/internal/service"
	regexp "github.com/dlclark/regexp2"
	"github.com/gin-gonic/gin"
)

const (
	emailRegexPattern = "^\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*$"
	// 和上面比起来,用 ` 看起来就比较清爽
	passwordRegexPattern = `^(?=.*[A-Za-z])(?=.*\d)(?=.*[$@$!%*#?&.])[A-Za-z\d$@$!%*#?&.]{8,72}$`
)

// UserHandlers 处理用户相关请求的结构体
type UserHandlers struct {
	emailRegExp    *regexp.Regexp
	passwordRegExp *regexp.Regexp
	svc            *service.UserService
}

// RegisterRoutes 注册用户相关路由
// 该方法用于将用户操作的路由绑定到对应的处理函数
func NewUserHandlers(svc *service.UserService) *UserHandlers {
	return &UserHandlers{
		emailRegExp:    regexp.MustCompile(emailRegexPattern, regexp.None),
		passwordRegExp: regexp.MustCompile(passwordRegexPattern, regexp.None),
		svc:            svc,
	}

}
func (h *UserHandlers) RegisterRoutes(routers *gin.Engine) {
	// 用户路由组
	rg := routers.Group("/users")
	// 注册路由
	rg.POST("/signup", h.SignUp)
	// 登录路由
	rg.POST("/sign", h.Login)
	// 用户资料路由
	rg.GET("/profile", h.Profile)
	// 编辑资料路由
	rg.POST("/edit", h.Edit)
}

// Signup 处理用户注册请求
// 该方法用于处理用户注册流程,包括验证数据,创建用户等
// 它接收一个 gin.Context 类型的参数,用于获取请求数据和发送响应
// 定义几个方法,包含注册、登录、编辑、查看
func (h *UserHandlers) SignUp(ctx *gin.Context) {
	// 注册
	// 字段标签,固定接收json格式,内部类
	type SignUpReq struct {
		Email           string `json:"email"`
		Password        string `json:"password"`
		ConfirmPassword string `json:"confirmPassword"`
	}
	//Bind 方法,类似是格式校验的一个方法,如果格式不是上面SignUp设置的格式就会报错。
	var req SignUpReq
	if err := ctx.Bind(&req); err != nil {
		ctx.String(200, "输入的格式不合法")
		return
	}
	// 邮箱格式校验
	isEmail, err := h.emailRegExp.MatchString(req.Email)
	if err != nil {
		ctx.String(http.StatusOK, "系统错误")
		return
	}
	if !isEmail {
		ctx.String(http.StatusOK, "邮箱式不对")
		return
	}
	// 密码格式校验
	isPassword, err := h.passwordRegExp.MatchString(req.Password)
	if err != nil {
		ctx.String(http.StatusOK, "系统错误")
		return
	}
	if !isPassword {
		ctx.String(http.StatusOK, "密码格式不对,密码长度应该不小于8位,且包含特殊符号")
		return
	}
	// 确认密码校验
	if req.Password != req.ConfirmPassword {
		ctx.String(200, "两次输入密码不一致")
		return
	}
	err = h.svc.SignUp(ctx, domain.User{
		Email:    req.Email,
		Password: req.Password,
	})
	switch {
	case err == nil:
		ctx.String(http.StatusOK, "注册成功,")
	case errors.Is(err, service.ErrDuplicateEmail):
		ctx.String(http.StatusOK, "邮箱冲突,请换一个")
	default:
		ctx.String(http.StatusOK, "系统错误")
	}

	ctx.String(http.StatusOK, "你已经完成注册")
}

// Login 处理用户登录请求
// 该方法用于处理用户登录流程,包括验证身份,生成会话等
// 它接收一个 gin.Context 类型的参数,用于获取请求数据和发送响应
// 定义几个方法,包含注册、登录、编辑、查看
func (h *UserHandlers) Login(ctx *gin.Context) {
	type LoginReq struct {
		Email    string `json:"email"`
		Password string `json:"password"`
	}
	//Bind 方法,类似是格式校验的一个方法,如果格式不是上面SignUp设置的格式就会报错。
	var req LoginReq
	if err := ctx.Bind(&req); err != nil {
		return
	}
	_, err := h.svc.Login(ctx, req.Email, req.Password)
	switch {
	case err == nil:
		ctx.String(http.StatusOK, "登录成功")
	case errors.Is(err, service.ErrInvalidUserOrPassword):
		ctx.String(http.StatusOK, "用户名称或者密码不对")
	default:
		ctx.String(http.StatusOK, "系统错误")
	}
}

// Profile 处理用户资料查询请求
// 该方法用于处理用户查询自己资料的请求
// 它接收一个 gin.Context 类型的参数,用于获取请求数据和发送响应
func (h *UserHandlers) Profile(ctx *gin.Context) {
	// 查看
}

// Edit 处理用户资料编辑请求
// 该方法用于处理用户编辑自己资料的请求
// 它接收一个 gin.Context 类型的参数,用于获取请求数据和发送响应
func (h *UserHandlers) Edit(ctx *gin.Context) {
	// 编辑
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值