前语

今天继续共享mayfly-go开源代码中代码或者是包组织形式。犹疑之后这儿不绘制传统UML图来描绘,直接用代码或许能更清晰。

开源项目地址:github.com/may-fly/may…

开源项目用到的,数据库结构是gorm, web结构是 gin,下面是关于用户(Account) 的相关规划和办法。

ModelBase 表结构根底类

项目依据gorm结构完成对数据库操作。

pkg/model/model.go 是数据模型根底类,里边封装了数据库应包括的根本字段和根本操作办法,实际创立表应该依据此结构进行承继。

Model界说

对应表结构上的特点便是:所有表都包括如下字段。

type Model struct {
   Id         uint64     `json:"id"`            // 记载唯一id
   CreateTime *time.Time `json:"createTime"`    // 关于创立者信息
   CreatorId  uint64     `json:"creatorId"`
   Creator    string     `json:"creator"`
   UpdateTime *time.Time `json:"updateTime"`    // 更新者信息
   ModifierId uint64     `json:"modifierId"`
   Modifier   string     `json:"modifier"`
}
// 将用户信息传入进来 填充模型。 这点作者是依据 m.Id===0 来判断是 新增 或者 修改。 这种写
// 法有个问题 有必要 先用数据实例化再去调用此办法,次序不能反。。
func (m *Model) SetBaseInfo(account *LoginAccount)

数据操作根本办法

// 下面办法  不是作为model的办法进行处理的。 办法都会用到 global.Db 也便是数据库连接
// 将一组操作封装到事务中进行处理。  办法封装很好。外部传入对应操作即可
func Tx(funcs ...func(db *gorm.DB) error) (err error) 
// 依据ID去表中查询希望得到的列。若error不为nil则为不存在该记载
func GetById(model interface{}, id uint64, cols ...string) error 
// 依据id列表查询
func GetByIdIn(model interface{}, list interface{}, ids []uint64, orderBy ...string) 
// 依据id列查询数据总量
func CountBy(model interface{}) int64 
// 依据id更新model,更新字段为model中不为空的值,即int类型不为0,ptr类型不为nil这类字段值
func UpdateById(model interface{}) error 
// 依据id删除model
func DeleteById(model interface{}, id uint64) error 
// 依据条件删除
func DeleteByCondition(model interface{}) 
// 刺进model
func Insert(model interface{}) error 
// @param list为数组类型 如 var users *[]User,可指定为非model结构体,即只包括需求回来的字段结构体
func ListBy(model interface{}, list interface{}, cols ...string) 
// @param list为数组类型 如 var users *[]User,可指定为非model结构体
func ListByOrder(model interface{}, list interface{}, order ...string) 
// 若 error不为nil,则为不存在该记载
func GetBy(model interface{}, cols ...string) 
// 若 error不为nil,则为不存在该记载
func GetByConditionTo(conditionModel interface{}, toModel interface{}) error 
// 依据条件 获取分页成果
func GetPage(pageParam *PageParam, conditionModel interface{}, toModels interface{}, orderBy ...string) *PageResult 
// 依据sql 获取分页目标
func GetPageBySql(sql string, param *PageParam, toModel interface{}, args ...interface{}) *PageResult 
// 通过sql取得列表参数
func GetListBySql(sql string, params ...interface{}) []map[string]interface{}
// 通过sql取得列表而且转化为模型
func GetListBySql2Model(sql string, toEntity interface{}, params ...interface{}) error
  • 模型界说 表根底字段,与根底设置办法。

  • 界说了对模型操作根本办法。会运用大局的global.Db 数据库连接。 数据库最终操作收敛点。

Entity 表实体

文件途径 internal/sys/domain/entity/account.go

Entity是承继于 model.Model。对根底字段进行扩展,进而完成一个表规划。 例如咱们用t_sys_account为例。

type Account struct {
   model.Model
   Username      string     `json:"username"`
   Password      string     `json:"-"`
   Status        int8       `json:"status"`
   LastLoginTime *time.Time `json:"lastLoginTime"`
   LastLoginIp   string     `json:"lastLoginIp"`
}
func (a *Account) TableName() string {
   return "t_sys_account"
}
// 是否可用
func (a *Account) IsEnable() bool {
   return a.Status == AccountEnableStatus
}

这样咱们就完成了 t_sys_account 表,在根底模型上,完善了表独有的办法。

相当于在根底表字段上 完成了 一个确定表的结构和办法。

Repository 库

文件途径 internal/sys/domain/repository/account.go

首要界说 与** 此单表相关的详细操作的接口(与详细事务相关联起来了)**

type Account interface {
   // 依据条件获取账号信息
   GetAccount(condition *entity.Account, cols ...string) error
   // 取得列表
   GetPageList(condition *entity.Account, pageParam *model.PageParam, toEntity interface{}, orderBy ...string) *model.PageResult
   // 刺进
   Insert(account *entity.Account)
   //更新
   Update(account *entity.Account)
}

界说 账号表操作相关 的根本接口,这儿并没有完成。 简单讲将来我这个类至少要支持哪些办法。

Singleton

文件途径 internal/sys/infrastructure/persistence/account_repo.go

是对Respository库实例化,他是一个单例形式。

type accountRepoImpl struct{} // 对Resposity 接口完成
// 这儿就很巧妙,用的是小写开头。 为什么呢??
func newAccountRepo() repository.Account {
   return new(accountRepoImpl)
}
// 办法详细完成 如下
func (a *accountRepoImpl) GetAccount(condition *entity.Account, cols ...string) error {
   return model.GetBy(condition, cols...)
}
func (m *accountRepoImpl) GetPageList(condition *entity.Account, pageParam *model.PageParam, toEntity interface{}, orderBy ...string) *model.PageResult {
}
func (m *accountRepoImpl) Insert(account *entity.Account) {
   biz.ErrIsNil(model.Insert(account), "新增账号信息失利")
}
func (m *accountRepoImpl) Update(account *entity.Account) {
   biz.ErrIsNil(model.UpdateById(account), "更新账号信息失利")
}

单例形式创立与运用

文件地址: internal/sys/infrastructure/persistence/persistence.go

// 项目初始化就会创立此变量
var accountRepo  = newAccountRepo()
// 通过get办法回来该实例
func GetAccountRepo() repository.Account { // 回来接口类型
   return accountRepo
}

界说了与Account相关的操作办法,而且以Singleton方式露出给外部运用。

App 事务逻辑办法

文件地址:internal/sys/application/account_app.go

在事务逻辑办法中,作者现已将接口 和 完成办法写在一个文件中了。 分隔的确太麻烦了。

界说事务逻辑办法接口

Account 事务逻辑模块相关办法集合。

type Account interface {
   GetAccount(condition *entity.Account, cols ...string) error
   GetPageList(condition *entity.Account, pageParam *model.PageParam, toEntity interface{}, orderBy ...string) *model.PageResult
   Create(account *entity.Account)
   Update(account *entity.Account)
   Delete(id uint64)
}

完成相关办法

// # 账号模型实例化, 对应账号操作办法.  这儿依然是 单例形式。
// 留意它入参是 上面 repository.Account 类型
func newAccountApp(accountRepo repository.Account) Account {
   return &accountAppImpl{
      accountRepo: accountRepo,
   }
}
type accountAppImpl struct {
   accountRepo repository.Account
}
func (a *accountAppImpl) GetAccount(condition *entity.Account, cols ...string) error {}
func (a *accountAppImpl) GetPageList(condition *entity.Account, pageParam *model.PageParam, toEntity interface{}, orderBy ...string) *model.PageResult {}
func (a *accountAppImpl) Create(account *entity.Account) {}
func (a *accountAppImpl) Update(account *entity.Account) {}
func (a *accountAppImpl) Delete(id uint64) {}

留意点:

  • 入参 repository.Account 是上面界说的根底操作办法

  • 依然是Singleton 形式

被单例化完成

在文件 internal/sys/application/application.go 中界说大局变量。

界说如下:

// 这儿将上面根本办法传入进去
var accountApp  = newAccountApp(persistence.GetAccountRepo()) 
func GetAccountApp() Account { // 回来上面界说的Account接口
   return accountApp
}

目前为止,咱们得到了关于 Account 相关事务逻辑操作。

运用于gin路由【最外层】

例如详细登录逻辑等。

文件途径: internal/sys/api/account.go

type Account struct {
   AccountApp  application.Account
   ResourceApp application.Resource
   RoleApp     application.Role
   MsgApp      application.Msg
   ConfigApp   application.Config
}
// @router /accounts/login [post]
func (a *Account) Login(rc *ctx.ReqCtx) {
   loginForm := &form.LoginForm{}              // # 取得表单数据,并将数据赋值给特定值的
   ginx.BindJsonAndValid(rc.GinCtx, loginForm) // # 验证值类型
   // 判断是否有开启登录验证码校验
   if a.ConfigApp.GetConfig(entity.ConfigKeyUseLoginCaptcha).BoolValue(true) { // # 从db中判断是不是需求验证码
      // 校验验证码
      biz.IsTrue(captcha.Verify(loginForm.Cid, loginForm.Captcha), "验证码过错") // # 用的Cid(密钥生成id 和 验证码去验证)
   }
   // # 用于解密取得原始暗码,这种加密办法对后端库来说,也是不行见的
   originPwd, err := utils.DefaultRsaDecrypt(loginForm.Password, true)
   biz.ErrIsNilAppendErr(err, "解密暗码过错: %s")
   // # 界说一个用户实体
   account := &entity.Account{Username: loginForm.Username}
   err = a.AccountApp.GetAccount(account, "Id", "Username", "Password", "Status", "LastLoginTime", "LastLoginIp")
   biz.ErrIsNil(err, "用户名或暗码过错(查询过错)")
   fmt.Printf("originPwd is: %v, %v\n", originPwd, account.Password)
   biz.IsTrue(utils.CheckPwdHash(originPwd, account.Password), "用户名或暗码过错")
   biz.IsTrue(account.IsEnable(), "该账号不行用")
   // 校验暗码强度是否契合
   biz.IsTrueBy(CheckPasswordLever(originPwd), biz.NewBizErrCode(401, "您的暗码安全等级较低,请修改后重新登录"))
   var resources vo.AccountResourceVOList
   // 获取账号菜单资源
   a.ResourceApp.GetAccountResources(account.Id, &resources)
   // 菜单树与权限code数组
   var menus vo.AccountResourceVOList
   var permissions []string
   for _, v := range resources {
      if v.Type == entity.ResourceTypeMenu {
         menus = append(menus, v)
      } else {
         permissions = append(permissions, *v.Code)
      }
   }
   // 保存该账号的权限codes
   ctx.SavePermissionCodes(account.Id, permissions)
   clientIp := rc.GinCtx.ClientIP()
   // 保存登录音讯
   go a.saveLogin(account, clientIp)
   rc.ReqParam = fmt.Sprintln("登录ip: ", clientIp)
   // 赋值loginAccount 首要用于记载操作日志,因为操作日志保存恳求上下文没有该信息不保存日志
   rc.LoginAccount = &model.LoginAccount{Id: account.Id, Username: account.Username}
   rc.ResData = map[string]interface{}{
      "token":         ctx.CreateToken(account.Id, account.Username),
      "username":      account.Username,
      "lastLoginTime": account.LastLoginTime,
      "lastLoginIp":   account.LastLoginIp,
      "menus":         menus.ToTrees(0),
      "permissions":   permissions,
   }
}

可以看出来,一个事务是由多个App组合起来共同来完成的。

详细运用的时候在router初始化时。

account := router.Group("sys/accounts")
a := &api.Account{
   AccountApp:  application.GetAccountApp(),
   ResourceApp: application.GetResourceApp(),
   RoleApp:     application.GetRoleApp(),
   MsgApp:      application.GetMsgApp(),
   ConfigApp:   application.GetConfigApp(),
}
// 绑定单例形式
account.POST("login", func(g *gin.Context) {
   ctx.NewReqCtxWithGin(g).
      WithNeedToken(false).
      WithLog(loginLog). // # 将日志挂到恳求目标中
      Handle(a.Login)   // 对应处理办法
})

总概览图

下图描绘了,从底层模型到上层调用的依赖关系链。

UML 图.jpg

问题来了: 实际开发中,应该怎么区别。

  • 归于模型的根底办法

  • 数据模型操作上的办法

  • 与独自模型相关的操作集

  • 与应用相关的办法集

区别隔他们才能知道代码方位写在哪里。