go mayfly開源項目代碼結構設計
前言
今天繼續(xù)分享mayfly-go開源代碼中代碼或者是包組織形式。猶豫之后這里不繪制傳統(tǒng)UML圖來描述,直接用代碼或許能更清晰。
開源項目地址:github.com/may-fly/may…
開源項目用到的,數(shù)據(jù)庫框架是gorm, web框架是 gin,下面是關于用戶(Account) 的相關設計和方法。
ModelBase 表結構基礎類
項目基于gorm框架實現(xiàn)對數(shù)據(jù)庫操作。
pkg/model/model.go 是數(shù)據(jù)模型基礎類,里面封裝了數(shù)據(jù)庫應包含的基本字段和基本操作方法,實際創(chuàng)建表應該基于此結構進行繼承。
Model定義
對應表結構上的特點就是:所有表都包含如下字段。
type Model struct {
Id uint64 `json:"id"` // 記錄唯一id
CreateTime *time.Time `json:"createTime"` // 關于創(chuàng)建者信息
CreatorId uint64 `json:"creatorId"`
Creator string `json:"creator"`
UpdateTime *time.Time `json:"updateTime"` // 更新者信息
ModifierId uint64 `json:"modifierId"`
Modifier string `json:"modifier"`
}
// 將用戶信息傳入進來 填充模型。 這點作者是根據(jù) m.Id===0 來判斷是 新增 或者 修改。 這種寫
// 法有個問題 必須 先用數(shù)據(jù)實例化再去調用此方法,順序不能反。。
func (m *Model) SetBaseInfo(account *LoginAccount)
數(shù)據(jù)操作基本方法
// 下面方法 不是作為model的方法進行處理的。 方法都會用到 global.Db 也就是數(shù)據(jù)庫連接
// 將一組操作封裝到事務中進行處理。 方法封裝很好。外部傳入對應操作即可
func Tx(funcs ...func(db *gorm.DB) error) (err error)
// 根據(jù)ID去表中查詢希望得到的列。若error不為nil則為不存在該記錄
func GetById(model interface{}, id uint64, cols ...string) error
// 根據(jù)id列表查詢
func GetByIdIn(model interface{}, list interface{}, ids []uint64, orderBy ...string)
// 根據(jù)id列查詢數(shù)據(jù)總量
func CountBy(model interface{}) int64
// 根據(jù)id更新model,更新字段為model中不為空的值,即int類型不為0,ptr類型不為nil這類字段值
func UpdateById(model interface{}) error
// 根據(jù)id刪除model
func DeleteById(model interface{}, id uint64) error
// 根據(jù)條件刪除
func DeleteByCondition(model interface{})
// 插入model
func Insert(model interface{}) error
// @param list為數(shù)組類型 如 var users *[]User,可指定為非model結構體,即只包含需要返回的字段結構體
func ListBy(model interface{}, list interface{}, cols ...string)
// @param list為數(shù)組類型 如 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
// 根據(jù)條件 獲取分頁結果
func GetPage(pageParam *PageParam, conditionModel interface{}, toModels interface{}, orderBy ...string) *PageResult
// 根據(jù)sql 獲取分頁對象
func GetPageBySql(sql string, param *PageParam, toModel interface{}, args ...interface{}) *PageResult
// 通過sql獲得列表參數(shù)
func GetListBySql(sql string, params ...interface{}) []map[string]interface{}
// 通過sql獲得列表并且轉化為模型
func GetListBySql2Model(sql string, toEntity interface{}, params ...interface{}) error
- 模型定義 表基礎字段,與基礎設置方法。
- 定義了對模型操作基本方法。會使用全局的global.Db 數(shù)據(jù)庫連接。 數(shù)據(jù)庫最終操作收斂點。
Entity 表實體
文件路徑 internal/sys/domain/entity/account.go
Entity是繼承于 model.Model。對基礎字段進行擴展,進而實現(xiàn)一個表設計。 例如我們用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
}
這樣我們就實現(xiàn)了 t_sys_account 表,在基礎模型上,完善了表獨有的方法。
相當于在基礎表字段上 實現(xiàn)了 一個確定表的結構和方法。
Repository 庫
文件路徑 internal/sys/domain/repository/account.go
主要定義 與** 此單表相關的具體操作的接口(與具體業(yè)務相關聯(lián)起來了)**
type Account interface {
// 根據(jù)條件獲取賬號信息
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)
}
定義 賬號表操作相關 的基本接口,這里并沒有實現(xiàn)。 簡單講將來我這個類至少要支持哪些方法。
Singleton
文件路徑 internal/sys/infrastructure/persistence/account_repo.go
是對Respository庫實例化,他是一個單例模式。
type accountRepoImpl struct{} // 對Resposity 接口實現(xiàn)
// 這里就很巧妙,用的是小寫開頭。 為什么呢??
func newAccountRepo() repository.Account {
return new(accountRepoImpl)
}
// 方法具體實現(xiàn) 如下
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), "更新賬號信息失敗")
}
單例模式創(chuàng)建與使用
文件地址: internal/sys/infrastructure/persistence/persistence.go
// 項目初始化就會創(chuàng)建此變量
var accountRepo = newAccountRepo()
// 通過get方法返回該實例
func GetAccountRepo() repository.Account { // 返回接口類型
return accountRepo
}
定義了與Account相關的操作方法,并且以Singleton方式暴露給外部使用。
App 業(yè)務邏輯方法
文件地址:internal/sys/application/account_app.go
在業(yè)務邏輯方法中,作者已經(jīng)將接口 和 實現(xiàn)方法寫在一個文件中了。
分開確實太麻煩了。
定義業(yè)務邏輯方法接口
Account 業(yè)務邏輯模塊相關方法集合。
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)
}
實現(xiàn)相關方法
// # 賬號模型實例化, 對應賬號操作方法. 這里依然是 單例模式。
// 注意它入?yún)⑹?上面 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) {}
注意點:
- 入?yún)?
repository.Account是上面定義的基礎操作方法 - 依然是Singleton 模式
被單例化實現(xiàn)
在文件 internal/sys/application/application.go 中定義全局變量。
定義如下:
// 這里將上面基本方法傳入進去
var accountApp = newAccountApp(persistence.GetAccountRepo())
func GetAccountApp() Account { // 返回上面定義的Account接口
return accountApp
}
目前為止,我們得到了關于 Account 相關業(yè)務邏輯操作。
使用于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{} // # 獲得表單數(shù)據(jù),并將數(shù)據(jù)賦值給特定值的
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數(shù)組
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,
}
}
可以看出來,一個業(yè)務是由多個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) // 對應處理方法
})
總概覽圖
下圖描述了,從底層模型到上層調用的依賴關系鏈。

問題來了: 實際開發(fā)中,應該怎么區(qū)分。
- 屬于模型的基礎方法
- 數(shù)據(jù)模型操作上的方法
- 與單獨模型相關的操作集
- 與應用相關的方法集
區(qū)分開他們才能知道代碼位置寫在哪里。
以上就是go mayfly開源項目代碼結構設計的詳細內容,更多關于go mayfly開源代碼結構的資料請關注腳本之家其它相關文章!
相關文章
淺析Go項目中的依賴包管理與Go?Module常規(guī)操作
這篇文章主要為大家詳細介紹了Go項目中的依賴包管理與Go?Module常規(guī)操作,文中的示例代碼講解詳細,對我們深入了解Go語言有一定的幫助,需要的可以跟隨小編一起學習一下2023-10-10
Go開發(fā)go-optioner工具實現(xiàn)輕松生成函數(shù)選項模式代碼
go-optioner?是一個在?Go?代碼中生成函數(shù)選項模式代碼的工具,可以根據(jù)給定的結構定義自動生成相應的選項代碼,下面就來聊聊go-optioner是如何使用的吧2023-07-07

