go mayfly开源项目代码结构设计
作者:用户6512516549724 发布时间:2024-02-20 04:51:17
前言
今天继续分享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) // 对应处理方法
})
总概览图
下图描述了,从底层模型到上层调用的依赖关系链。
问题来了: 实际开发中,应该怎么区分。
属于模型的基础方法
数据模型操作上的方法
与单独模型相关的操作集
与应用相关的方法集
区分开他们才能知道代码位置写在哪里。
来源:https://juejin.cn/post/7165692262427885598


猜你喜欢
- 经常有网友会问,SQL Server占用了太多的内存,而且还会不断的增长;或者说已经设置了使用内存,可它没有用到那么多,这是怎么一回事儿呢?
- 1 包简介1.1 工作空间go语言的工作空间必须由 bin、pkg、src三个目录组成,可以在GOPATH环境变量中添加多个工作空间,但不能
- 为什么训练误差比测试误差高很多?一个Keras的模型有两个模式:训练模式和测试模式。一些正则机制,如Dropout,L1/L2正则项在测试模
- 起因:学校运河杯报了个项目,制作一个天气预测的装置。我用arduino跑了BME280模块,用蓝牙模块实现两块arduino主从机透传。但是
- 最近微信迎来了一次重要的更新,允许用户对”发现”页面进行定制。不知道从什么时候开始,微信朋友圈变得越来越复杂,当越来越多的人选择”仅展示最近
- 这最近在PJ的function库里看到的这个函数,感觉思路差了点,不过相对比较完美,只是闭合标签时的顺序问题,呵呵 修改一下数组arrTag
- 如何更改 pandas dataframe 中两列的位置:把其中的某列移到第一列的位置。原来的 df 是:df = pd.read_csv(
- 引言本文想要解决的问题是当DataFrame中某一列元素为不定长度的数组时,该如何对它们进行拆分分解为后续元素,从而进行进一步的提取操作,数
- 楼主在做公司项目的时候遇到url重定向的问题,因此上网简单查找,作出如下结果由于使用的是语言是python所以以下是python的简单解决方
- 编写兼容IE和FireFox的脚本确定的件很烦人的事,今日又经历了一次。一、正式表达式问题试图用以下表达式提取中括号“]”后面的内容,连接调
- 一切皆是对象在 Python 一切皆是对象,包括所有类型的常量与变量,整型,布尔型,甚至函数。 参见stackoverflow上的一个问题
- 前言:大家在写代码的时候,经常会使用print打印日志方便排查问题,然而print的问题就是太过简单,缺少时间、日志级别等格式化信息。Pyt
- 如下所示:ffmpeg中文文档:http://linux.51yip.com/search/ffmpegffmpeg -i test_bao
- Pycharm安装cv2 [python3.6]python解释器为Anaconda的3.6版本下载在这里选择对应的版本进行下载,其中参数分
- 在图像处理以及图像特效中,经常会用到一种成高斯分布的蒙版,蒙版可以用来做图像融合,将不同内容的两张图像结合蒙版,可以营造不同的艺术效果。这里
- 本文实例为大家分享了Python 12306抢火车票的具体代码,供大家参考,具体内容如下# -*- coding: utf-8 -*-fro
- 最近在内部讨论关于”完美三栏”的话题,看到一篇”In Search of the Holy Grail“,相当的好.故此翻译之.In Sea
- 一、join函数(一)参数使用说明描述Python join() 方法用于将序列中的元素以指定的字符连接生成一个新的字符串。语法join()
- 一、报错信息:【file】【Default Settint】---Project Interpreter 点击+搜索suds安装模块报错解决
- python-tkinter 实现各种个样的撩妹鼠标拖尾,效果图展示:系统的拖尾已经无法满足我们了,女朋友叫你把鼠标拖尾换成她的照片,让你时