go mayfly開源項(xiàng)目代碼結(jié)構(gòu)設(shè)計(jì)
前言
今天繼續(xù)分享mayfly-go開源代碼中代碼或者是包組織形式。猶豫之后這里不繪制傳統(tǒng)UML圖來描述,直接用代碼或許能更清晰。
開源項(xiàng)目地址:github.com/may-fly/may…
開源項(xiàng)目用到的,數(shù)據(jù)庫框架是gorm, web框架是 gin,下面是關(guān)于用戶(Account) 的相關(guān)設(shè)計(jì)和方法。
ModelBase 表結(jié)構(gòu)基礎(chǔ)類
項(xiàng)目基于gorm框架實(shí)現(xiàn)對數(shù)據(jù)庫操作。
pkg/model/model.go 是數(shù)據(jù)模型基礎(chǔ)類,里面封裝了數(shù)據(jù)庫應(yīng)包含的基本字段和基本操作方法,實(shí)際創(chuàng)建表應(yīng)該基于此結(jié)構(gòu)進(jìn)行繼承。
Model定義
對應(yīng)表結(jié)構(gòu)上的特點(diǎn)就是:所有表都包含如下字段。
type Model struct { Id uint64 `json:"id"` // 記錄唯一id CreateTime *time.Time `json:"createTime"` // 關(guān)于創(chuàng)建者信息 CreatorId uint64 `json:"creatorId"` Creator string `json:"creator"` UpdateTime *time.Time `json:"updateTime"` // 更新者信息 ModifierId uint64 `json:"modifierId"` Modifier string `json:"modifier"` } // 將用戶信息傳入進(jìn)來 填充模型。 這點(diǎn)作者是根據(jù) m.Id===0 來判斷是 新增 或者 修改。 這種寫 // 法有個(gè)問題 必須 先用數(shù)據(jù)實(shí)例化再去調(diào)用此方法,順序不能反。。 func (m *Model) SetBaseInfo(account *LoginAccount)
數(shù)據(jù)操作基本方法
// 下面方法 不是作為model的方法進(jìn)行處理的。 方法都會(huì)用到 global.Db 也就是數(shù)據(jù)庫連接 // 將一組操作封裝到事務(wù)中進(jìn)行處理。 方法封裝很好。外部傳入對應(yīng)操作即可 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結(jié)構(gòu)體,即只包含需要返回的字段結(jié)構(gòu)體 func ListBy(model interface{}, list interface{}, cols ...string) // @param list為數(shù)組類型 如 var users *[]User,可指定為非model結(jié)構(gòu)體 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ù)條件 獲取分頁結(jié)果 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獲得列表并且轉(zhuǎn)化為模型 func GetListBySql2Model(sql string, toEntity interface{}, params ...interface{}) error
- 模型定義 表基礎(chǔ)字段,與基礎(chǔ)設(shè)置方法。
- 定義了對模型操作基本方法。會(huì)使用全局的global.Db 數(shù)據(jù)庫連接。 數(shù)據(jù)庫最終操作收斂點(diǎn)。
Entity 表實(shí)體
文件路徑 internal/sys/domain/entity/account.go
Entity是繼承于 model.Model。對基礎(chǔ)字段進(jìn)行擴(kuò)展,進(jìn)而實(shí)現(xiàn)一個(gè)表設(shè)計(jì)。 例如我們用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 }
這樣我們就實(shí)現(xiàn)了 t_sys_account 表,在基礎(chǔ)模型上,完善了表獨(dú)有的方法。
相當(dāng)于在基礎(chǔ)表字段上 實(shí)現(xiàn)了 一個(gè)確定表的結(jié)構(gòu)和方法。
Repository 庫
文件路徑 internal/sys/domain/repository/account.go
主要定義 與** 此單表相關(guān)的具體操作的接口(與具體業(yè)務(wù)相關(guān)聯(lián)起來了)**
type Account interface { // 根據(jù)條件獲取賬號(hào)信息 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) }
定義 賬號(hào)表操作相關(guān) 的基本接口,這里并沒有實(shí)現(xiàn)。 簡單講將來我這個(gè)類至少要支持哪些方法。
Singleton
文件路徑 internal/sys/infrastructure/persistence/account_repo.go
是對Respository庫實(shí)例化,他是一個(gè)單例模式。
type accountRepoImpl struct{} // 對Resposity 接口實(shí)現(xiàn) // 這里就很巧妙,用的是小寫開頭。 為什么呢?? func newAccountRepo() repository.Account { return new(accountRepoImpl) } // 方法具體實(shí)現(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), "新增賬號(hào)信息失敗") } func (m *accountRepoImpl) Update(account *entity.Account) { biz.ErrIsNil(model.UpdateById(account), "更新賬號(hào)信息失敗") }
單例模式創(chuàng)建與使用
文件地址: internal/sys/infrastructure/persistence/persistence.go
// 項(xiàng)目初始化就會(huì)創(chuàng)建此變量 var accountRepo = newAccountRepo() // 通過get方法返回該實(shí)例 func GetAccountRepo() repository.Account { // 返回接口類型 return accountRepo }
定義了與Account相關(guān)的操作方法,并且以Singleton方式暴露給外部使用。
App 業(yè)務(wù)邏輯方法
文件地址:internal/sys/application/account_app.go
在業(yè)務(wù)邏輯方法中,作者已經(jīng)將接口 和 實(shí)現(xiàn)方法寫在一個(gè)文件中了。
分開確實(shí)太麻煩了。
定義業(yè)務(wù)邏輯方法接口
Account 業(yè)務(wù)邏輯模塊相關(guān)方法集合。
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) }
實(shí)現(xiàn)相關(guān)方法
// # 賬號(hào)模型實(shí)例化, 對應(yīng)賬號(hào)操作方法. 這里依然是 單例模式。 // 注意它入?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) {}
注意點(diǎn):
- 入?yún)?
repository.Account
是上面定義的基礎(chǔ)操作方法 - 依然是Singleton 模式
被單例化實(shí)現(xiàn)
在文件 internal/sys/application/application.go 中定義全局變量。
定義如下:
// 這里將上面基本方法傳入進(jìn)去 var accountApp = newAccountApp(persistence.GetAccountRepo()) func GetAccountApp() Account { // 返回上面定義的Account接口 return accountApp }
目前為止,我們得到了關(guān)于 Account 相關(guān)業(yè)務(wù)邏輯操作。
使用于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) // # 驗(yàn)證值類型 // 判斷是否有開啟登錄驗(yàn)證碼校驗(yàn) if a.ConfigApp.GetConfig(entity.ConfigKeyUseLoginCaptcha).BoolValue(true) { // # 從db中判斷是不是需要驗(yàn)證碼 // 校驗(yàn)驗(yàn)證碼 biz.IsTrue(captcha.Verify(loginForm.Cid, loginForm.Captcha), "驗(yàn)證碼錯(cuò)誤") // # 用的Cid(密鑰生成id 和 驗(yàn)證碼去驗(yàn)證) } // # 用于解密獲得原始密碼,這種加密方法對后端庫來說,也是不可見的 originPwd, err := utils.DefaultRsaDecrypt(loginForm.Password, true) biz.ErrIsNilAppendErr(err, "解密密碼錯(cuò)誤: %s") // # 定義一個(gè)用戶實(shí)體 account := &entity.Account{Username: loginForm.Username} err = a.AccountApp.GetAccount(account, "Id", "Username", "Password", "Status", "LastLoginTime", "LastLoginIp") biz.ErrIsNil(err, "用戶名或密碼錯(cuò)誤(查詢錯(cuò)誤)") fmt.Printf("originPwd is: %v, %v\n", originPwd, account.Password) biz.IsTrue(utils.CheckPwdHash(originPwd, account.Password), "用戶名或密碼錯(cuò)誤") biz.IsTrue(account.IsEnable(), "該賬號(hào)不可用") // 校驗(yàn)密碼強(qiáng)度是否符合 biz.IsTrueBy(CheckPasswordLever(originPwd), biz.NewBizErrCode(401, "您的密碼安全等級較低,請修改后重新登錄")) var resources vo.AccountResourceVOList // 獲取賬號(hào)菜單資源 a.ResourceApp.GetAccountResources(account.Id, &resources) // 菜單樹與權(quán)限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) } } // 保存該賬號(hào)的權(quán)限codes ctx.SavePermissionCodes(account.Id, permissions) clientIp := rc.GinCtx.ClientIP() // 保存登錄消息 go a.saveLogin(account, clientIp) rc.ReqParam = fmt.Sprintln("登錄ip: ", clientIp) // 賦值loginAccount 主要用于記錄操作日志,因?yàn)椴僮魅罩颈4嬲埱笊舷挛臎]有該信息不保存日志 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, } }
可以看出來,一個(gè)業(yè)務(wù)是由多個(gè)App組合起來共同來完成的。
具體使用的時(shí)候在router初始化時(shí)。
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) // 對應(yīng)處理方法 })
總概覽圖
下圖描述了,從底層模型到上層調(diào)用的依賴關(guān)系鏈。
問題來了: 實(shí)際開發(fā)中,應(yīng)該怎么區(qū)分。
- 屬于模型的基礎(chǔ)方法
- 數(shù)據(jù)模型操作上的方法
- 與單獨(dú)模型相關(guān)的操作集
- 與應(yīng)用相關(guān)的方法集
區(qū)分開他們才能知道代碼位置寫在哪里。
以上就是go mayfly開源項(xiàng)目代碼結(jié)構(gòu)設(shè)計(jì)的詳細(xì)內(nèi)容,更多關(guān)于go mayfly開源代碼結(jié)構(gòu)的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Gin golang web開發(fā)模型綁定實(shí)現(xiàn)過程解析
這篇文章主要介紹了Gin golang web開發(fā)模型綁定實(shí)現(xiàn)過程解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-10-10教你一分鐘配置好Go語言開發(fā)環(huán)境(多種操作系統(tǒng))
在這篇文章中,我們從頭到尾一步步指導(dǎo)你配置Golang開發(fā)環(huán)境,并編寫你的第一個(gè)"Hello,?World!"程序,我們詳細(xì)解釋了在多種操作系統(tǒng)(包括Windows、Linux和macOS)下的安裝過程、環(huán)境變量設(shè)置以及如何驗(yàn)證安裝是否成功2023-09-09Go語言對前端領(lǐng)域的入侵WebAssembly運(yùn)行原理
這篇文章主要為大家介紹了不安分的Go語言對Web?前端領(lǐng)域的入侵WebAssembly運(yùn)行原理實(shí)例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07Go語言實(shí)現(xiàn)的樹形結(jié)構(gòu)數(shù)據(jù)比較算法實(shí)例
這篇文章主要介紹了Go語言實(shí)現(xiàn)的樹形結(jié)構(gòu)數(shù)據(jù)比較算法,實(shí)例分析了樹形結(jié)構(gòu)數(shù)據(jù)比較算法的實(shí)現(xiàn)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-02-02使用Lumberjack+zap進(jìn)行日志切割歸檔操作
這篇文章主要介紹了使用Lumberjack+zap進(jìn)行日志切割歸檔操作,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-12-12詳解Golang函數(shù)式選項(xiàng)(Functional?Options)模式
什么是函數(shù)式選項(xiàng)模式,為什么要這么寫,這個(gè)編程模式解決了什么問題呢?其實(shí)就是為了解決動(dòng)態(tài)靈活的配置不同的參數(shù)的問題。下面通過本文給大家介紹Golang函數(shù)式選項(xiàng)(Functional?Options)模式的問題,感興趣的朋友一起看看吧2021-12-12golang網(wǎng)絡(luò)通信超時(shí)設(shè)置方式
這篇文章主要介紹了golang網(wǎng)絡(luò)通信超時(shí)設(shè)置方式,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-12-12