开发者

go mayfly开源项目代码结构设计

目录
  • 前言
  • ModelBase 表结构基础类
    • Model定义
    • 数据操作基本方法
  • Entity 表实体
    • Repository 库
      • Singleton
        • App 业务逻辑方法
          • 定义业务逻辑方法接口
          • 实现相关方法
          • 被单例化实现
        • 使用于gin路由【最外层】
          • 总概览图

            前言

            今天继续分享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, to编程Model 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.Aphpccount, 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(accwww.devze.comountRepoImpl)
            }
            // 方法具体实现 如下
            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(loginFhttp://www.devze.comorm.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 = &mo开发者_JAVA学习del.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)   // 对应处理方法
            })
            

            总概览图

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

            go mayfly开源项目代码结构设计

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

            • 属于模型的基础方法
            • 数据模型操作上的方法
            • 与单独模型相关的操作集
            • 与应用相关的方法集

            区分开他们才能知道代码位置写在哪里。

            以上就是go mayfly开源项目代码结构设计的详细内容,更多关于go mayfly开源代码结构的资料请关注我们其它相关文章!

            0

            上一篇:

            下一篇:

            精彩评论

            暂无评论...
            验证码 换一张
            取 消

            最新开发

            开发排行榜