(原) QOR: Golang开发的电商系统和CMS工具库--读官方文档

原创文章,请后转载,并注明出处。

QOR: Golang开发的电商系统和CMS工具库--读官方文档

QOR: Golang开发的电商系统和CMS工具库--示例学习


基于Go语言开发的电商系统和CMS的SDK,根据网友的说法:QOR可以看作是PHP中的ThinkPHP,Python中的Django。

Github

官方文档 https://doc.getqor.com/

中文版 https://getqor.com/cn

网友专栏介绍学习,不过看文章是2018年的了。相关的文章有限,估计又得啃外文学习。

https://github.com/golangpkg/qor-cms-demos 各章示例代码,虽然也是3年前的。

首先,QOR还不算是CMS,它比起Web框架来说进行了更进一步的封装,将Web开发中常用的部分封装成用起来更简单的库。用它来创建一个基于内容管理的Web应用更加快速简单。

开始

package main

import (
	"fmt"
	"net/http"

	"github.com/jinzhu/gorm"
	_ "github.com/jinzhu/gorm/dialects/sqlite"
	"github.com/qor/admin"
)

// 用户
type User struct {
	gorm.Model
	Name string
}

// 产品
type Product struct {
	gorm.Model
	Name        string
	Description string
}

func main() {
	// 注册数据库,可以是sqlite3 也可以是 mysql 换下驱动就可以了。
	DB, _ := gorm.Open("sqlite3", "demo.db")
	DB.AutoMigrate(&User{}, &Product{}) //自动创建表。

	// 初始化admin 还有其他的,比如API
	Admin := admin.New(&admin.AdminConfig{DB: DB})

	// 创建admin后台对象资源。
	Admin.AddResource(&User{})
	Admin.AddResource(&Product{})

	// 启动服务
	mux := http.NewServeMux()
	Admin.MountTo("/admin", mux)
	fmt.Println("Listening on: 9000")
	http.ListenAndServe(":9000", mux)
}

打开浏览器 http://127.0.0.1:9000/admin,一个简单的后台就建立起来了。

在后端将显示一个Users菜单,可以添加User。只有一个项目为Name。产品项也一样。就这么几句,就实现了一个简单的后台。


认证

QOR提供了一个认证系统Auth,它是一个模块化的认证系统,用于Golang的web开发,它提供了不同的认证后端来加速你的开发。

目前认证有数据库密码,github,谷歌,facebook, twitter认证支持,并且很容易添加其他支持基于Auth的提供者接口。国内还应该扩展qq认证,微信认证,支付宝认证。

要使用它,基本流程是:

使用配置初始化身份验证
注册一些提供商
将其注册到路由器
package main

import (
	"net/http"

	"github.com/jinzhu/gorm"
	_ "github.com/jinzhu/gorm/dialects/sqlite"
	"github.com/qor/admin"
	"github.com/qor/auth"
	"github.com/qor/auth/auth_identity"
	"github.com/qor/auth/providers/facebook"
	"github.com/qor/auth/providers/github"
	"github.com/qor/auth/providers/google"
	"github.com/qor/auth/providers/password"
	"github.com/qor/auth/providers/twitter"
	"github.com/qor/session/manager"
)

var (
	gormDB, _ = gorm.Open("sqlite3", "sample.db") //初始化数据库

	Auth  = auth.New(&auth.Config{DB: gormDB}) //认证初始化
	Admin = admin.New(&admin.AdminConfig{DB: gormDB})
)

func init() {
	gormDB.AutoMigrate(&auth_identity.AuthIdentity{}, &auth_identity.AuthIdentity{}) //设置认证模块,自动创建表。

	// 注册身份验证提供程序
	// 允许使用用户名/密码
	Auth.RegisterProvider(password.New(&password.Config{}))

	// 允许使用Github
	Auth.RegisterProvider(github.New(&github.Config{
		ClientID:     "github client id",
		ClientSecret: "github client secret",
	}))

	// 允许使用Google
	Auth.RegisterProvider(google.New(&google.Config{
		ClientID:     "google client id",
		ClientSecret: "google client secret",
	}))

	// 允许使用Facebook
	Auth.RegisterProvider(facebook.New(&facebook.Config{
		ClientID:     "facebook client id",
		ClientSecret: "facebook client secret",
	}))

	// 允许使用Twitter
	Auth.RegisterProvider(twitter.New(&twitter.Config{
		ClientID:     "twitter client id",
		ClientSecret: "twitter client secret",
	}))

	// 创建admin后台对象资源。
	Admin.AddResource(&auth_identity.AuthIdentity{})
}

func main() {
	mux := http.NewServeMux()
	Admin.MountTo("/admin", mux)
	mux.Handle("/auth/", Auth.NewServeMux()) //路由
	http.ListenAndServe(":9000", manager.SessionManager.Middleware(mux))
}

admin http://localhost:9000/admin/auth_identities

注册  http://localhost:9000/auth/register

登陆  http://localhost:9000/auth/login

以上示例在官方文档基础上作了修改才通过运行,郁闷。注册和登陆界面也没有显示表单等,倒是后台能添加用户。

Auth有两个模型,模型AuthIdentityModel用于保存登录信息,模型UserModel用于保存用户信息。之所以将身份验证和用户信息保存为两种不同的模型,是因为希望能够将用户链接到多个身份验证信息记录,这样用户就可以有多种登录方式。

如果您不需要这样做,那么您可以将这两个模型设置为相同的一个,或者跳过设置UserModel。就看示例中gormDB.AutoMigrate(&auth_identity.AuthIdentity{}, &auth_identity.AuthIdentity{})

不同的提供者(你可以在后台看到provider)通常使用不同的信息登录,例如提供者password使用用户名/密码,github使用github用户ID,因此对于每个提供者,它将这些信息保存到自己的记录中。

不需要设置AuthIdentityModel,Auth具有默认的AuthIdentityModel定义。如果需要修改,则在库中的auth_identity表修改。

默认情况下,没有定义UserModel,即使您仍然可以使用身份验证功能,身份验证将返回使用身份验证信息记录作为登录的用户。

但是通常情况下,您的应用程序将具有User模型,在设置其值之后,当您从任何提供商注册新帐户时,Auth将使用UserStorer创建/获取用户,并将其ID链接到auth身份记录。

自定义视图

认证使用Render来渲染页面,可以引用它来了解如何注册func映射,扩展视图路径。如果想把它编译到包中一起,需要有BinddataFS

如果要优先使用视图路径,则可以将其添加到ViewPaths中,如果您想覆盖默认的(丑陋的)登录/注册页面,可以看诸如https://github.com/qor/auth_themes之类的身份验证主题。

发送邮件

使用Mailer进行验证以发送电子邮件,默认情况下,Auth会将打印的电子邮件打印到控制台,请配置它发送真实的电子邮件。

用户存储

Auth根据您的AuthIdentityModel,创建一个默认的UserStorer来获取/保存用户。更改它可以实现自己的用户存储。

会话存储

Auth还有一个处理会话、flash消息的默认方法,可以通过实现会话存储接口来覆盖。

默认情况下,Auth使用会话的默认管理器保存数据到cookie,但为了正确保存cookie,你必须注册会话的中间件到你的路由器,例如:

func main() {
    mux := http.NewServeMux()

    // Register Router
    mux.Handle("/auth/", Auth.NewServeMux())
    http.ListenAndServe(":9000", manager.SessionManager.Middleware(mux))
}

重定向器

在一些身份验证操作之后,如登录、注册或确认,身份验证将用户重定向到某个URL,您可以配置用重定向器重定向哪个页面,默认情况下,将重定向到主页。

如果您想重定向到上次访问的页面,可以使用redirect_back。

var RedirectBack = redirect_back.New(&redirect_back.Config{
    SessionManager:  manager.SessionManager,
    IgnoredPrefixes: []string{"/auth"},
}

var Auth = auth.New(&auth.Config{
    ...
    Redirector: auth.Redirector{RedirectBack},
})

为了使其正常工作,redirect_back需要将每个访问的最后访问的URL保存到与会话管理器的会话中,这意味着您需要将redirect_back和SessionManager的中间件安装到路由器中。

http.ListenAndServe(":9000", manager.SessionManager.Middleware(RedirectBack.Middleware(mux)))

主题

为了节省更多开发人员的努力,已经创建了一些认证主题。它通常具有设计良好的页面,如果您不需要太多自定义要求,则可以只用几行就可以使Auth系统准备好用于您的应用程序,例如:

import "github.com/qor/auth_themes/clean"

var Auth = clean.New(&auth.Config{
    DB:         db.DB,
    Render:     config.View,
    Mailer:     config.Mailer,
    UserModel:  models.User{},
})

授权

身份验证是验证你是谁的过程,授权是验证你能访问某个东西的过程。认证包不仅提供认证


Admin

QOR Admin是一个Golang框架,允许您在几分钟内创建一个美丽的,跨平台的,可配置的管理界面,管理您的数据。

常规配置

您可以在初始化Admin时使用AdminConfig struct自定义Admin,以下是一些通用配置

type AdminConfig struct {
  SiteName       string
  DB             *gorm.DB
  Auth           Auth
  SessionManager session.ManagerInterface
  I18n           I18n
  AssetFS        assetfs.Interface
  *Transformer
}

Admin := admin.New(&admin.AdminConfig{SiteName: “Qor Example”})

AssetFS定义了呈现页面时如何查找模板

仪表盘

QOR Admin提供了一个默认的仪表板页面,其中包含一些虚拟文本。 如果要自定义仪表板,则可以在QOR视图路径中创建一个文件dashboard.tmpl,QOR Admin在呈现仪表板页面时会将其作为Golang模板加载。

如果要禁用仪表板,可以将其重定向到其他页面

Admin.GetRouter().Get("/", func(c *admin.Context) {
  http.Redirect(c.Writer, c.Request, "/admin/clients", http.StatusSeeOther)
})

资源

资源可以通过QOR管理员的用户界面(通常是GORM后端模型)进行管理。

将资源添加到QOR Admin
// GORM-backend model
// 这里的顺序也将影响后台的显示顺序
type User struct {
  gorm.Model
  Email     string
  Password  string
  Name      sql.NullString
  Gender    string
  Role      string
  Addresses []Address  //在实际运行中,提示没有定义Address
}

// Add it to Admin
user := Admin.AddResource(&User{}, &admin.Config{Menu: []string{"User Management"}})  //实际运行中,将此改为中文将失去前导用户图标
资源配置

在admin.Config中自定义资源时可用的选项是:

名称 类型 缺省 说明
Name string 显示资源的名称
Menu []string 资源的菜单设置
Permission *roles.Permission 控制资源的权限
Themes []ThemeInterface 设置资源的自定义主题
Priority int 控制菜单中的显示顺序,按ASC顺序排列
Singleton bool false 设置资源是单个对象还是多个对象。
Invisible bool false 设置资源在菜单中是否可见
PageCount int 20 分页设置,设置每页显示多少条记录

字段

自定义可见字段

默认情况下,所有字段都可见。如果您明确声明字段,则只有定义的字段可见,并且它们将按定义的顺序显示:

// 将资源“Order”,“Product”添加到管理
order := Admin.AddResource(&models.Order{})
product := Admin.AddResource(&models.Product{})

// 显示给定的属性
order.IndexAttrs("User", "PaymentAmount", "ShippedAt", "CancelledAt", "State", "ShippingAddress")

// 除 `State` 外显示所有
order.IndexAttrs("-State")

// 设置属性将显示在新页面中
order.NewAttrs("User", "PaymentAmount", "ShippedAt", "CancelledAt", "State", "ShippingAddress")

// 除 `State` 外显示所有
order.NewAttrs("-State")

// 使用“Section”构造新表单,使其整洁干净
product.NewAttrs(
  &admin.Section{
    Title: "Basic Information",
    Rows: [][]string{
      {"Name"},
      {"Code", "Price"},
    }
  },
  &admin.Section{
    Title: "Organization",
    Rows: [][]string{
      {"Category", "Collections", "MadeCountry"},
    }
  },
  "Description",
  "ColorVariations",
}

// 设置属性将显示在编辑页面上,类似于新页面
order.EditAttrs("User", "PaymentAmount", "ShippedAt", "CancelledAt", "State", "ShippingAddress")

// 设置属性将显示在显示页面上,类似于新页面
// 如果尚未配置ShowAttrs,则不会生成任何显示页面,而是显示编辑表单
order.ShowAttrs("User", "PaymentAmount", "ShippedAt", "CancelledAt", "State", "ShippingAddress")

我修改了以上的示例代码,但依然提示“product.NewAttrs is not a type”。

自定义嵌套资源的字段
order := Admin.AddResource(&models.Order{})
orderItemMeta := order.Meta(&admin.Meta{Name: "OrderItems"})
orderItemResource := orderItemMeta.Resource

orderItemResource.EditAttrs("ProductCode", "Price", "Quantity")

元Meta

默认情况下,资源的字段是根据其类型和关系呈现的。 默认值应满足通常情况,您可以通过覆盖Meta定义来自定义呈现。

这是一个有关使用户的“性别”作为用户表单中的选择元素的示例,该元素包含三个选项“男性”,“女性”和“未知”。

user.Meta(&admin.Meta{Name: “Gender”, Config: &admin.SelectOneConfig{Collection: []string{“Male”, “Female”, “Unknown”}}})

自定义元

如果字段的默认配置不符合您的需求,则您想自定义字段:

type Meta struct {
    Name            string
    FieldName       string
    Label           string
    Type            string
    Setter          func(object interface{}, metaValue *resource.MetaValue, context *qor.Context)
    Valuer          func(object interface{}, context *qor.Context) (value interface{})
    FormattedValuer func(object interface{}, context *qor.Context) (formattedValue interface{})
    Permission      *roles.Permission
    Config          MetaConfigInterface
    Collection      interface{}
    Resource        *Resource
}

. Name 要覆盖的字段的名称

user.Meta(&admin.Meta{Name: “Gender”, Config: &admin.SelectOneConfig{Collection: []string{“Male”, “Female”, “Unknown”}}})

. FieldName 映射到资源中的属性名称,通常不需要设置,并且默认情况下与Name相同。

当您要将QOR Admin用作RESTFul API服务并公开具有不同名称的字段时,通常需要这样做,例如:

order.Meta(&admin.Meta{Name: “Code”, FieldName: “ExternalCode”})

. Type 属性的显示类型

. Label 表单中属性的标签和索引页的表标题。

默认情况下 “address” 标签是 “Address”.

. Setter Setter定义了如何将表单值解码为字段,这是QOR Admin生成的默认Setter,它从metaValue获取表单值,并根据字段的类型对该值进行解码以进行记录。

. Valuer Valuer定义了如何从对象中获取字段的值,它返回一个golang对象作为结果,QOR通常会根据字段的值和状态以不同的方式呈现字段模板。

. FormattedValuer FormattedValuer与Valuer类似,但它通常返回格式化的字符串作为结果,它将在索引页和API中显示给最终用户。

. Resource 这可用于自定义嵌套形式的属性,通常无需设置,请查看如何自定义嵌套形式的属性以获取详细信息。

. Permission 定义此属性的用户权限

. Config 当前属性类型的配置

&admin.SelectOneConfig{Collection: []string{“Male”, “Female”, “Unknown”}}

虚拟字段

您可以配置QOR Admin以显示“虚拟”字段—这些字段不是数据库属性。

如果你想在NewAttrs, EditAttrs, ShowAttrs中使用虚拟字段,你必须这样做:

定义元数据的Type

定义元的Setter
常见的元类型
String/Text
Checkbox
Number/Float
Date/Datetime
Hidden/Readonly
Password
Rich editor
Single edit
Collection edit  (集合编辑)
Select one
Select many
user.Meta(&admin.Meta{Name: "Password",
    Type:   "password",
    Valuer: func(interface{}, *qor.Context) interface{} { return "" },
    Setter: func(record interface{}, metaValue *resource.MetaValue, context *qor.Context) {
        if newPassword := utils.ToString(metaValue.Value); newPassword != "" {
            bcryptPassword, _ := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
            record.(*models.User).EncryptedPassword = string(bcryptPassword)
        }
    },
})

搜索&范围&过滤器

搜索

您可以使用SearchAttrs配置资源的可搜索属性,如果未设置SearchAttrs,则将使用资源的IndexAttrs进行搜索

product.SearchAttrs("Name", "Code", "Category.Name", "Brand.Name")
将资源添加到管理员搜索中心

QOR管理员提供了一个搜索中心,你可以用AddSearchResource注册可搜索资源,有了搜索中心,你可以在一个请求中搜索多个资源

Admin.AddSearchResource(product, user, order)

您可以覆盖它,用自定义资源的搜索

oldSearchHandler := product.SearchHandler
product.SearchHandler = func(keyword string, context *qor.Context) *gorm.DB {
    context.SetDB(context.GetDB().Preload("Variations.Color").Preload("Variations.Size").Preload("Variations.Material"))
    return oldSearchHandler(keyword, context)
}
范围
user.Scope(&admin.Scope{Name: "Active", Handler: func(db *gorm.DB, context *qor.Context) *gorm.DB {
  return db.Where("active = ?", true)
}})

在列表左上角显示按钮,进行部份属性过滤显示。

组范围

将相似的作用域放入一个组,请为其设置组名称

order.Scope(&admin.Scope{Name: "Paid", Group: "State", Handler: func(db *gorm.DB, context *qor.Context) *gorm.DB {
    return db.Where("state = ?", "paid")
}})

order.Scope(&admin.Scope{Name: "Shipped", Group: "State", Handler: func(db *gorm.DB, context *qor.Context) *gorm.DB {
  return db.Where("state = ?", "shipped")
}})

默认范围

默认范围将应用于所有请求

  order.Scope(&admin.Scope{
    Name: "Default Scope",
    Default: true,
    Handler: func(db *gorm.DB, context *qor.Context) *gorm.DB {
      return db.Where("state = ?", "paid")
    },
  })
根据条件的可见范围

基于可见的返回true使范围可见

order.Scope(&admin.Scope{Name: "Paid", Group: "State",
  Visible: func(context *admin.Context) bool {
    return context.CurrentUser.IsAdmin
  },
  Handler: func(db *gorm.DB, context *qor.Context) *gorm.DB {
      return db.Where("state = ?", "paid")
  },
})

基于条件的可见过滤器
user.Filter(&admin.Filter{
  Name: "Gender",
  Visible: func(context *admin.Context) bool {
    return context.CurrentUser.IsAdmin
  },
  Config: &admin.SelectOneConfig{
    Collection: []string{"Male", "Female", "Unknown"},
  },
})
过滤

在给定的设置下,在QOR Admin中使任何资源可过滤。

以下示例显示了如何在假设的项目中按性别(“男”,“女”或“未知”)过滤用户。

// Filter users by gender
user.Filter(&admin.Filter{
  Name: "Gender",
  Config: &admin.SelectOneConfig{
    Collection: []string{"Male", "Female", "Unknown"},
  },
})

// Filter products by collection
product.Filter(&admin.Filter{
    Name:   "Collections",
    Config: &admin.SelectOneConfig{RemoteDataResource: collection},
})

操作

使用

让我们为用户定义一个Enable操作,查看Action的工作方式。

type User struct {
  gorm.Model
  Name   string
  Active bool
}

user := Admin.AddResource(&models.User{})

user.Action(&admin.Action{
  Name: "enable",
  Handle: func(actionArgument *admin.ActionArgument) error {
    // `FindSelectedRecords` => 在批量操作模式下,将返回所有已检查的记录,在其他模式下,将返回当前记录
    for _, record := range actionArgument.FindSelectedRecords() {
      actionArgument.Context.DB.Model(record.(*models.User)).Update("Active", true)
    }
    return nil
  },
})

用户的索引和编辑页面将显示一个按钮“ENABLE”,就像这样

配置
type Action struct {
    Name        string
    Label       string
    Method      string
    URL         func(record interface{}, context *Context) string
    URLOpenType string
    Visible     func(record interface{}, context *Context) bool
    Handler     func(argument *ActionArgument) error
    Modes       []string
    Resource    *Resource
    Permission  *roles.Permission
}

Method 方法,HTTP方法,默认设置为PUT。当有URL选项时,默认为GET

URL 设置URL,动作按钮将触发请求。此选项将覆盖处理程序选项。例如,通过URL检查动作。

URLOpenType 设置打开URL的方式。具有Resource的操作的默认值为bottomsheet(链接将在弹出窗口中打开)。 对于没有资源的动作是_blank。

Visible 设置此操作何时可见的条件,例如,根据条件检查“可见操作”。

Handler 处理动作请求的函数

Permission 权限控制

Resource 设置资源以存储用户输入。

Modes 支持5个选项:“batch”批处理、“edit”编辑、“show”显示、“menu_item”菜单项、collection集合,5种模式映射到这些页面:

batch,当启用大容量编辑模式时,大容量操作将显示在索引列表页中

collection,将显示在索引列表页面中
	
show,显示页面动作,将显示在显示页面中。

edit,编辑表单操作,将显示在编辑页面上。

menu_item,菜单项操作,将显示在表的菜单中。
基于条件的可见动作

仅当订单处于“draft”和“processing”状态时,使用“Visible”控制显示“Cancel”功能,可以用来取消订单。“Visible”选项的记录参数是当前订单, 当返回值为false时,该操作(Cancel)对用户不可见。

order.Action(&admin.Action{
    Name: "Cancel",
    Handler: func(argument *admin.ActionArgument) error {
      // cancel the order
    },
    Visible: func(record interface{}, context *Context) bool {
      if order, ok := record.(*models.Order); ok {
        for _, state := range []string{"draft", "processing"} {
          if order.State == state {
            return true
          }
        }
      }
      return false
    },
    Modes: []string{"show", "menu_item"},
  })
URL动作

此示例显示了如何执行一项操作,使用户可以单击该操作以查看前端的产品详细信息页面。 URL函数的record参数是当前产品,我们通过产品代码将URL设置为前端。

  product.Action(&admin.Action{
    Name: "View On Site",
    URL: func(record interface{}, context *admin.Context) string {
      if product, ok := record.(*models.Product); ok {
        return fmt.Sprintf("/products/%v", product.Code)
      }
      return "#"
    },
    Modes: []string{"menu_item", "edit", "show"},
  })
批处理

使用QOR Admin创建批处理操作相当容易,只需使用“操作模式”在索引页面上启用某个操作,该操作便成为批处理操作,例如:

  order.Action(&admin.Action{
    Name: "Cancel",
    Handler: func(argument *admin.ActionArgument) error {
      // cancel the order
    },
    Visible: func(record interface{}, context *Context) bool {
      if order, ok := record.(*models.Order); ok {
        for _, state := range []string{"draft", "processing"} {
          if order.State == state {
            return true
          }
        }
      }
      return false
    },
    Modes: []string{"batch"},
  })
用户输入的动作

您需要定义一个资源来接受用户的输入,可以在Handler函数中使用它。

// ship 行为参数
type trackingNumberArgument struct {
  TrackingNumber string
}

trackingNumberRes := Admin.NewResource(&trackingNumberArgument{})

order.Action(&admin.Action{
  Name: "Ship",
  Handler: func(argument *admin.ActionArgument) error {
    // 从参数获取用户输入。
    trackingNumberArgument := argument.Argument.(*trackingNumberArgument)
    for _, record := range argument.FindSelectedRecords() {
      argument.Context.GetDB().Model(record).UpdateColumn("tracking_number", trackingNumberArgument.TrackingNumber)
    }
    return nil
  },
  Resource: trackingNumberRes,
  Modes: []string{"show", "menu_item"},
})

数据处理与验证

验证方式

应用程序级别验证

如果要对应用程序进行一些全局验证,则可以使用QOR验证,它是GORM扩展,可以在创建,更新时用于验证模型。

管理员级别验证

如果您只想验证来自QOR Admin的数据,那么QOR Admin Validator适合您,它将在将表单/ JSON数据解码到结构之前检查数据。

store := Admin.AddResource(&Store{})

store.AddValidator(&resource.Validator{
  Name: "check_has_name" // 注册另一个具有相同名称的验证器将覆盖先前的验证器
  Handler: func(record interface{}, metaValues *resource.MetaValues, context *qor.Context) error {
    // 从metaValues获取meta的值,metaValues是保存所有发布数据的结构
    if meta := metaValues.Get("Name"); meta != nil {
        if name := utils.ToString(meta.Value); strings.TrimSpace(name) == "" {
            return validations.NewError(record, "Name", "Name can't be blank")
        }
    }
    return nil
  },
})
在保存到数据库之前处理数据

应用程序级处理器

如果您想在将数据保存到数据库之前处理一些数据,并将其全局保存,那么GORM回调非常适合您的情况。

管理员级别处理器

但是,当您只想处理来自QOR Admin的数据时,可以使用QOR Admin Processor,它可以在将数据解码到结构中之后处理数据,但是在将它们保存到数据库中之前,可以像这样使用它:

store.AddProcessor(&resource.Processor{
  Name: "process_store_data", // 注册另一个具有相同名称的处理器将覆盖先前的处理器
    Handler: func(value interface{}, metaValues *resource.MetaValues, context *qor.Context) error {
        if store, ok := value.(*Store); ok {
      // do something...
        }
        return nil
    },
})

RESTFul API

QOR管理员为注册资源生成RESTFul API

基本用法
func main() {
  API := admin.New(&qor.Config{DB: db.DB})
  user := API.AddResource(&User{})

  mux := http.NewServeMux()
  API.MountTo("/api", mux)
  http.ListenAndServe(":3000", mux); err != nil {
}

一旦你定义了你的用户资源,QOR管理员将为它生成RESTFul API

GET /api/users - 检索用户列表
GET /api/users/12 - 检索特定用户
POST /api/users - 创建一个新用户
PUT /api/users/12 - 更新用户 #12
DELETE /api/users/12 - 删除用户 #12

请求扩展名为.json的API,例如/api/users.json,QOR Admin将返回JSON格式的数据,扩展名为.xml(/api/users.xml)的请求将以XML strucutre返回数据。

(两种格式都有了,啥操作也不用管,简单实用。看看该如何控制权限。)

自定义字段

API中的自定义字段与普通资源相同,你可以使用IndexAttrs, ShowAttrs来为API用户配置可见字段。

动作

有时,需要公开API中固有的非RESTful操作。

此类操作的一个示例是您要为资源引入状态更改,可以使用QOR Admin进行设计,例如:

user.Action(&admin.Action{
  Name: "enable",
  Handle: func(actionArgument *admin.ActionArgument) error {
    // `FindSelectedRecords` => in bulk action mode, will return all checked records, in other mode, will return current record
    for _, record := range actionArgument.FindSelectedRecords() {
      actionArgument.Context.DB.Model(record.(*models.User)).Update("Active", true)
    }
    return nil
  },
})

user.Action(&admin.Action{
  Name: "disable",
  Handle: func(actionArgument *admin.ActionArgument) error {
    // `FindSelectedRecords` => in bulk action mode, will return all checked records, in other mode, will return current record
    for _, record := range actionArgument.FindSelectedRecords() {
      actionArgument.Context.DB.Model(record.(*models.User)).Update("Active", false)
    }
    return nil
  },
})

它将生成如下的API:

PUT /api/users/12/enable - enable user #12
PUT /api/users/12/disable - disable user #12
嵌套的API
type User struct {
    gorm.Model
    Name                   string `form:"name"`
    Orders                 []Order
}

type Order struct {
    gorm.Model
    UserID     uint
    User       User
    Amount     float32
    OrderItems []OrderItem
}

user := API.AddResource(&User{})
// 使用用户的字段名注册嵌套的API
userOrders, _ := user.AddSubResource("Orders")

这将生成API:

GET /api/users/12/orders - Retrieves a list of orders from user #12
GET /api/users/12/orders/22 - Retrieves order #22 from user #12
POST /api/users/12/orders - Creates a new order for user #12
PUT /api/users/12/orders/22 - Updates user #12's orders #22
DELETE /api/users/12/orders/22 - Deletes users #12's orders #22
认证与授权

与普通管理网站相同,您可以使用身份验证和授权来保护您的API。

并使用Permission进行资源级别,字段级别权限控制

认证与授权

认证方式

QOR Admin通过为常见的Authentication相关任务提供界面,允许您集成当前的身份验证方法。

您需要做的是实现如下所示的认证接口,并在QOR Admin值中设置它。

type Auth interface {
  GetCurrentUser(*Context) qor.CurrentUser // 获取当前用户,如果没有权限,返回nil
  LoginURL(*Context) string // 获取登录URL,如果没有权限,将重定向到该URL
  LogoutURL(*Context) string // 获取注销URL,如果单击管理界面中的注销链接,将访问此页面
}

在初始化QOR Admin时进行设置时,例如:

func main() {
  // 初始化QOR Admin时设置身份验证界面
  Admin := admin.New(&admin.AdminConfig{
    Auth: yourAuthInterface,
  })
}

下面是一个整合了QOR Auth和QOR Auth主题的例子:

import "github.com/qor/auth_themes/clean"

var Auth = clean.New(&auth.Config{
  DB:         DB,
  // User model needs to implement qor.CurrentUser interface (https://godoc.org/github.com/qor/qor#CurrentUser) to use it in QOR Admin
  UserModel:  models.User{},
})

type AdminAuth struct {}

func (AdminAuth) LoginURL(c *admin.Context) string {
    return "/auth/login"
}

func (AdminAuth) LogoutURL(c *admin.Context) string {
    return "/auth/logout"
}

func (AdminAuth) GetCurrentUser(c *admin.Context) qor.CurrentUser {
    currentUser := Auth.GetCurrentUser(c.Request)
    if currentUser != nil {
      qorCurrentUser, ok := currentUser.(qor.CurrentUser)
      if !ok {
        fmt.Printf("User %#v haven't implement qor.CurrentUser interface\n", currentUser)
      }
      return qorCurrentUser
    }
    return nil
}

func main() {
  // Set Auth interface when initialize QOR Admin
  Admin := admin.New(&admin.AdminConfig{
    Auth: &AdminAuth{},
  })
}
授权
资源授权
Admin.AddResource(&Product{}, &admin.Config{
  Permission: roles.Deny(roles.Delete, roles.Anyone).Allow(roles.Delete, "admin")
})
字段授权
product := Admin.AddResource(&Product{})

product.Meta(&admin.Meta{Name: "Price", Permission: roles.Allow(roles.Update, "admin")})
行为授权

QOR Admin将检查权限模式角色。在检查当前用户是否具有调用操作的能力时更新,其他模式将被忽略。

user.Action(&admin.Action{
  Name: "enable",
  Permission: roles.Allow(roles.Update, "admin"),
  Handle: func(actionArgument *admin.ActionArgument) error {
    // `FindSelectedRecords` => in bulk action mode, will return all checked records, in other mode, will return current record
    for _, record := range actionArgument.FindSelectedRecords() {
      actionArgument.Context.DB.Model(record.(*models.User)).Update("Active", true)
    }
    return nil
  },
})
菜单授权

QOR Admin将检查权限模式角色。在检查当前用户是否具有查看菜单的能力时读取,其他模式将被忽略。

Admin.AddMenu(&admin.Menu{Name: “Report”, Link: “/admin”, Permission: roles.Allow(roles.Read, “admin”)})

国际化

QOR Admin提供了一个支持国际化的接口

type I18n interface {
    Scope(scope string) I18n
    Default(value string) I18n
    T(locale string, key string, args ...interface{}) template.HTML
}

如果您在初始化QOR Admin时指定了实现此接口的i18n后端,它将立即获得国际化支持。

Admin := admin.New(&admin.AdminConfig{
  I18n: yourI18nBackend,
})
QOR I18n

QOR I18n实现了I18n界面,下面让我向您展示如何在Qor Admin中使用它。

首先,使用存储初始化i18n。 您可以一起使用多个存储,较早的存储具有更高的优先级。 因此,在此示例中,I18n将首先在数据库中查找翻译,然后如果未找到,则继续在YAML文件中找到它。

import (
    "github.com/jinzhu/gorm"
    "github.com/qor/i18n"
    "github.com/qor/i18n/backends/database"
    "github.com/qor/i18n/backends/yaml"
  )

  func main() {
    db, _ := gorm.Open("mysql", "user:password@/dbname?charset=utf8&parseTime=True&loc=Local")

    I18n := i18n.New(
      database.New(&db), // load translations from the database
      yaml.New(filepath.Join(config.Root, "config/locales")), // 从config/locales目录中加载YAML文件翻译
    )
  }

然后使用I18n初始化QOR Admin

  Admin := admin.New(&admin.AdminConfig{
    I18n: I18n,
  })

完成了! 所有翻译将由I18n后端提供支持。

通过QOR管理界面管理I18n翻译

没有人愿意通过更改数据库来更新翻译或直接更新YAML,因此i18n提供了一个友好的Web界面,用于通过QOR Admin管理翻译,使其生效,只需将其作为资源添加到QOR Admin中即可,例如:

Admin.AddResource(I18n)

如果您将I18n作为资源注册到QOR Admin,则可以在初始化时跳过配置QOR Admin,i18n会自动执行

主题与定制

全局样式表/脚本代码

QOR管理员将根据您管理员的SiteName自动加载javascript和样式表文件

例如,假设您将站点名称设置为Qor Demo,则QOR管理员将查找 {qor view paths}/assets/javascripts/qor_demo.js 和{qor view paths}/assets/stylesheets/qor_demo.css,如果存在则加载它们。

视图路径

当QOR管理呈现页面时,它用AssetFS查找模板。

AssetFS的默认实现是使用预先注册的视图路径从文件系统查找模板。

它包括:

{current_path}/app/views/qor
{current_path}/vendor/github.com/qor/admin/views
$GOPATH/src/github.com/qor/admin/views
主题

QOR管理提供灵活的模板定制。您可以为资源定义自己的主题。

QOR Admin中资源的自定义主题可以使用自定义javascript和CSS文件。

要应用自定义主题,请使用UseTheme方法设置主题名称,将从主题路径载入 assets/javascripts/fancy.js 和 assets/stylesheets/fancy.css

例如:

product := Admin.AddResource(&Product{})
product.UseTheme("fancy")

这意味着当请求product页面时,将加载 {qor view paths}/themes/fancy/assets/stylesheets/fancy.css 和 {qor view paths}/themes/fancy/assets/javascripts/fancy.js

自定义模板

QOR管理员正在使用go模板来渲染管理界面,默认模板可以从这里找到 https://github.com/qor/admin/tree/master/views

您可能想要根据您的需求定制其中的一些。你可以把一个同名的新模板放到QOR视图路径中,QOR管理员会根据优先级加载模板。

QOR管理员将从这些路径查找模板,顶级路径将有更高的优先级

{qor view paths}/themes/{theme name}/{resource params}/{template}
{qor view paths}/themes/{theme name}/{template}
{qor view paths}/{resource params}/{template}
{qor view paths}/{template}

覆写layout.tmpl将影响整个网站

你可以创建文件名layout.tmpl放到{current_path}/app/views/qor

覆写layout.tmpl只影响一个资源

如果要覆盖指定资源的布局,你可以把文件放到 {qor view paths}/{resource's params}。例如:覆写Product的布局, 创建 layout.tmpl 文件,把它放在 {current_path}/app/views/qor/products 目录

覆写layout.tmpl影响集合资源

使用UseTheme为这些资源设置相同的主题名称

res.UseTheme('fancy') 将文件layout.tmpl放到 {current_path}/app/views/qor/themes/fancy/layout.tmpl
菜单

QOR管理提供了一个灵活的方式来管理菜单。默认情况下,资源将列在菜单的顶层。您可以手动设置位置。

Admin.AddResource(&User{})

Admin.AddResource(&Product{}, &admin.Config{Menu: []string{"Product Management"}})
Admin.AddResource(&Color{}, &admin.Config{Menu: []string{"Product Management"}})
Admin.AddResource(&Size{}, &admin.Config{Menu: []string{"Product Management"}})

Admin.AddResource(&Order{}, &admin.Config{Menu: []string{"Order Management"}})

如果不希望在菜单中显示资源,请使用Invisible选项

Admin.AddResource(&User{}, &admin.Config{Invisible: true})
用自定义URL注册菜单

您可以添加自定义URL菜单,像这样:

// 注册菜单 `Sales Report`, 在 "Reports" 下
Admin.AddMenu(&admin.Menu{Name: "Sales Report", Link: "/admin/sales_report", Ancestors: []string{"Reports"}})

// 注册菜单 `Sales Report` 并带相对路径, 最后的URL将是admin路径+RelativePath, 本例中为 `/admin/sales_report`
Admin.AddMenu(&admin.Menu{Name: "Sales Report", RelativePath: "/sales_report", Ancestors: []string{"Reports"}})
菜单权限
// 注册菜单有权限,用户有“admin”权限可以看到“Report”菜单。
Admin.AddMenu(&admin.Menu{Name: "Report", Link: "/admin/reports", Permission: roles.Allow(roles.Read, "admin")})
优先菜单

用优先级设置菜单的优先级,小的数字优先级高,负数优先级低

Admin.AddMenu(&admin.Menu{Name: "First Menu", Priority: 1})
Admin.AddMenu(&admin.Menu{Name: "Second Menu", Priority: 2})
Admin.AddMenu(&admin.Menu{Name: "Third Menu", Priority: 5})
Admin.AddMenu(&admin.Menu{Name: "Forth Menu", Priority: -2})
Admin.AddMenu(&admin.Menu{Name: "Last Menu", Priority: -1})
配置自己的菜单图标

QOR管理员使用来自 material icons 的图标,并在每个菜单上预先生成属性名称。所以很容易定制自己的菜单图标。

假设您正在向产品添加图标。首先,去材料图标页面,选择你想要的图标,然后复制内容在截图

然后把它放在CSS中,关于如何为QOR管理自定义CSS去查看QOR管理主题。

[qor-icon-name*="Products"] > a::before {
  content: "\E2BF";
}

如果你不想使用资源名作为图标名。你可以像这样使用IconName

  Admin.AddResource(&User{}, &admin.Config{IconName: "YouOwnIconName"})

菜单图标对应的css将是

[qor-icon-name*="YouOwnIconName"] > a::before {
  content: "\E2BF";
}

扩展QOR管理

扩展QOR资源

将结构添加到QOR Admin后,QOR Admin将检查此结构及其嵌入式结构是否实现了接口ConfigureResourceBeforeInitializeInterface或ConfigureResourceInterface

ConfigureResourceBeforeInitializeInterface接口将在初始化资源之前被调用。

ConfigureResourceInterface接口将在初始化资源之后被调用。

因此,当AddResource时,工作流如下所示:

type User struct {
}

func (User) ConfigureQorResourceBeforeInitialize(resource.Resourcer) {
  // 初始化前做一些事
}

func (User) ConfigureQorResource(resource.Resourcer) {
  // 初始化后做一些事
}

user := Admin.AddResource(&User{})
// 1, 执行 User.ConfigureQorResourceBeforeInitialize(user)
// 2, 对资源应用默认设置
// 3, 执行 User.ConfigureQorResource(user)

当编写QOR插件时,这是很有帮助的,大多数插件都是基于此编写的,例如:QOR L10n, QOR Publish2

覆盖CURD处理程序

QOR Admin基于GORM的API生成默认的CURD处理程序,如果您的资源不是GORM后端模型,则可以考虑编写自己的CRUD处理程序,例如将其保存到Redis或缓存服务器中,例如:

res.FindOneHandler = func(result interface{}, metaValues *resource.MetaValues, context *qor.Context) error {
  // 查找记录并将其解码以得到结果
}

res.FindManyHandler = func(results interface{}, context *qor.Context) error {
  // 查找记录并将其解码为结果(找到多个记录)
}

res.SaveHandler = func(result interface{}, context *qor.Context) error {
  // 保存结果
}

res.DeleteHandler = func(result interface{}, context *qor.Context) error {
  // 删除结果
}

查看https://github.com/qor/qor/blob/master/resource/crud.go文件,可以从默认实现中获取一些提示。

属性

如您所知,您可以使用IndexAttrs,NewAttrs,EditAttrs,ShowAttrs来设置索引/显示/编辑/新页面的属性。

在编写插件时,您可能会要求始终显示或隐藏一些属性,OverrideIndexAttrs,OverrideNewAttrs,OverrideEditAttrs,OverrideShowAttrs可以发挥作用,您可以这样写:

// 每次您为资源配置EditAttrs时,我们都会附加字段`PublisReady`并从编辑属性中删除`State`。
res.OverrideEditAttrs(func() {
  res.EditAttrs(res.EditAttrs(), "PublishReady", "-State")
})
Metas

QOR Admin将结合您的元配置,最新的配置将覆盖之前的配置。

user.Meta(&admin.Meta{Name: "Gender", Label: "Select Gender", Config: &admin.SelectOneConfig{Collection: []string{"Male", "Female", "Unknown"}}})
user.Meta(&admin.Meta{Name: "Gender", Config: &admin.SelectOneConfig{Collection: []string{"Male", "Female"}}})

// 变成
user.Meta(&admin.Meta{Name: "Gender", Label: "Select Gender", Config: &admin.SelectOneConfig{Collection: []string{"Male", "Female"}}})
注册Meta处理器

Meta处理将在每次重新配置Meta都被调用

genderMeta := user.Meta(&admin.Meta{Name: "Gender", Label: "Select Gender", Config: &admin.SelectOneConfig{Collection: []string{"Male", "Female"}}})

genderMeta.AddProcessor(*admin.MetaProcessor{
  Name: "make-sure-label-is-select-gender",
  Handler: func(meta *admin.Meta) {
    meta.Label = "Select Gender"
  },
})
创建新的Meta类型

QOR管理员只提供常见的元类型,您可以轻松创建自己的元类型,如:

user.Meta(&admin.Meta{Name: "FieldName", Type: "my-fancy-meta-type"})

然后创建模板meta/index/my-fancy-meta-type.tmpl,meta/show/my-fancy-meta-type.tmpl,然后将它们放入qor视图路径,即可完成。

meta/index/my-fancy-meta-type.tmpl将在呈现索引页面时使用,如果不存在,QOR Admin将使用Valuer中的meta值,并将其显示为列表中的字符串。

meta/form/my-fancy-meta-type.tmpl将在渲染显示/编辑页面时使用,该文件必须存在才能正确渲染meta。

QOR Slug有示例

创建Meta配置

如果要传递某些配置以供查看,Meta Config适合您,不同类型的Metas通常具有不同的配置内容,例如meta select one,您可以配置其数据源,打开类型,对于meta rich editor,您可以配置其使用的插件,资产管理器。

对于你创建的元类型,如果你需要将配置传递给视图,最好为它创建一个元配置,例如:

type FancyMetaConfig struct {
  Config1 string
  Config2 string
}

// 元配置必须实现这个接口
func (FancyMetaConfig) ConfigureQorMeta(metaor resource.Metaor) {
  if meta, ok := metaor.(*admin.Meta); ok {
    // do something for meta
  }
}

Rich Editor Config可以看看示例

默认Meta配置器

Meta Configor是全局注册到Admin中的东西,以后注册的任何meta都会调用Meta Configor,例如:

// 如果未配置,所有`date`元数据将获得默认的FormattedValuer。
Admin.RegisterMetaConfigor("date", func(meta *Meta) {
    if meta.FormattedValuer == nil {
        meta.SetFormattedValuer(func(value interface{}, context *qor.Context) interface{} {
            switch date := meta.GetValuer()(value, context).(type) {
            case *time.Time:
                if date == nil {
                    return ""
                }
                if date.IsZero() {
                    return ""
                }
                return utils.FormatTime(*date, "2006-01-02", context)
            case time.Time:
                if date.IsZero() {
                    return ""
                }
                return utils.FormatTime(date, "2006-01-02", context)
            default:
                return date
            }
        })
    }
})

Meta Configors示例

自定义视图

customize templates 跳转到视图一章去学习

注册FuncMap

注册功能映射到视图,然后你可以在你的模板中使用它们。

Admin.RegisterFuncMap("my_fancy_func", func() string {
  return "my_fancy_func"
})
查看动作

如果您将任何模板放在{qor view path}/actions中,则会自动将其加载到索引/编辑/新建/显示页面。

您只能通过创建模板{qor view path}/actions/index/my_html_snippet.tmpl来为索引页面加载HTML代码段,该代码段将被加载到页面的subheader中。

QOR Activity, QOR Publish2是基于此策略构建的。

查看标题的操作

如果您将模板放入{qor view path} /actions/header中,它将被加载到管理网站的顶部区域,例如:

QOR Help,QOR Notification 是基于此实现的。

路由

您可以使用路由器定义自己的路由。

路由(也称为多路复用器,处理程序)是一种从URL路径映射到某些代码的方法,当最终用户访问该代码时,该代码就会执行。

注册HTTP路由

首先,获取 router:

router := Admin.GetRouter()
普通路由
router.Get("/path", func(context *admin.Context) {
  // do something here
})

router.Post("/path", func(context *admin.Context) {
  // do something here
})

router.Put("/path", func(context *admin.Context) {
  // do something here
})

router.Delete("/path", func(context *admin.Context) {
  // do something here
})
命名路由
router.Get("/path/:name", func(context *admin.Context) {
  context.Request.URL.Query().Get(":name")
})
正则表达式支持
router.Get("/path/:name[world]", func(context *admin.Context) { // "/hello/world"
  context.Request.URL.Query().Get(":name")
})

router.Get("/path/:name[\\d+]", func(context *admin.Context) { // "/hello/123"
  context.Request.URL.Query().Get(":name")
})
中间件

QOR管理员的路由器有中间件支持,你可以用它做一些高级的工作,以下面的代码为例:

db1 := gorm.Open("sqlite", "db1.db")
db2 := gorm.Open("sqlite", "db2.db")

Admin.GetRouter().Use(&admin.Middleware{
  Name: "switch_db",
  Handler: func(context *admin.Context, middleware *admin.Middleware) {
    // 将与产品相关的请求的管理员数据库切换到db2
    if regexp.MustCompile("/admin/products").MatchString(context.Request.URL.Path) {
      context.SetDB(db2)
    }
    middleware.Next(context)
  },
})

与WEB框架集成

QOR Admin应该能够与大多数golang Web框架集成,以下是一些如何与它们集成的示例。

与HTTP ServeMux集成
mux := http.NewServeMux()
Admin.MountTo("/admin", mux)
http.ListenAndServe(":9000", mux)
与Beego整合
mux := http.NewServeMux()
Admin.MountTo("/admin", mux)
beego.Handler("/admin/*", mux)
beego.Run()
与Gin集成
mux := http.NewServeMux()
Admin.MountTo("/admin", mux)

r := gin.Default()
r.Any("/admin/*resources", gin.WrapH(mux))
r.Run()
与 gorilla/mux集成
adminMux := http.NewServeMux()
Admin.MountTo("/admin", adminMux)

r := mux.NewRouter()
r.PathPrefix("/admin").Handler(adminMux)

http.Handle("/", r)

部署到生产

部署应用程序时,许多人都喜欢二进制部署,它具有很多优点,这也是我们建议的部署应用程序的方式。

但是,正如您所知,在查找模板时,QOR Admin的默认AssetFS实现来自文件系统,这意味着这些模板必须存在于生产服务器上,否则将导致找不到模板错误。

我们构建QOR Bindatafs可以帮助它,它可以将QOR模板编译成二进制文件,请参阅其README以获得进一步的帮助。


缓存存储

缓存存储提供了一种方法来缓存web内容,以加快您的网站的速度。

Memcached缓存存储使用情况

import "github.com/qor/cache/memcached"

func main() {
  client := memcached.New(&Config{Hosts: []string{"127.0.0.1:11211"}, NameSpace: "qor_demo_v1"})

  // 保存值' Hello World '与键' Hello World '到缓存存储
  err := client.Set("hello_world", "Hello World")

  // 通过键“ hello_world”获取节省的值
  result, err := client.Get("hello_world")

  // 将user的编组后的值保存到缓存存储中
  err := client.Set("user", user)

  // 将保存的值解组到user2
  err := client.Unmarshal("user", &user2)

  //使用`hello_world`键获取保存的值; 如果密钥不存在,则返回的`func`结果将被保存到缓存存储区中
  result, err := client.Fetch("hello_world", func() interface{} {
    return "..."
  })

  // 删除保存的值
  err := client.Delete(key string)
}

内存缓存存储使用情况

import "github.com/qor/cache/memory"

func main() {
  client := memory.New()
  // 与memcached缓存存储区使用相同的API
}

Redis缓存存储使用情况

import "github.com/qor/cache/redis"

func main() {
  client = New(&redis.Options{Addr: "127.0.0.1:6379",
    Password: "",   // no password set
    DB:       0,    // use default DB
    PoolSize: 100,
  })
  // 与memcached缓存存储区使用相同的API
}

BindataFS

BindataFS可以用来利用go-bindata将模板编译成二进制文件

安装BindataFS

go get -u -f github.com/qor/bindatafs/…

初始化项目的BindataFS,设置要存储BindataFS相关文件的路径,例如 config/bindatafs:

bindatafs config/bindatafs

用法

import "<your_project>/config/bindatafs"

func main() {
  assetFS := bindatafs.AssetFS

  // 将视图路径注册到AssetFS
  assetFS.RegisterPath("<your_project>/app/views")
  assetFS.RegisterPath("<your_project>/vender/plugin/views")

  // 将注册视图路径下的模板编译为二进制
  assetFS.Compile()

  // 获取文件内容
  fileContent, ok := assetFS.Asset("home/index.tmpl")
}

您需要在运行go build之前使用Compile方法将模板编译到go文件中,并且如果更改了任何模板,则需要重新编译它。

如果您使用标签bindatafs启动应用程序,AssetFS将访问生成的go文件或当前二进制文件中的文件

go run -tags ‘bindatafs’ main.go

否则它将从文件系统的已注册视图路径访问其内容,这对于本地开发更容易

go run main.go

使用命名空间

尽管您可以启动多个assetfs软件包来容纳来自不同视图路径的模板(模板名称可能有冲突),以用于不同的用例,但是Bindatafs为您提供了一个更简单的解决方案。

func main() {
  // 生成具有名称空间的子AssetFS
  adminAssetFS := assetFS.NameSpace("admin_related_files")

  // 将视图路径注册到这个名称空间
  adminAssetFS.RegisterPath("<your_project>/app/admin_views")

  // 访问注册下的文件,查看当前名称空间的路径
  adminAssetFS.Asset("admin_view.tmpl")
}

QOR Admin 使用BindataFS

import "<your_project>/config/bindatafs"

func main() {
  Admin = admin.New(&qor.Config{DB: db.Publish.DraftDB()})
  Admin.SetAssetFS(bindatafs.AssetFS.NameSpace("admin"))
}

QOR Render 使用BindataFS

import  "github.com/qor/render"

func main() {
  View := render.New()
  View.SetAssetFS(assetFS.NameSpace("views"))
}

QOR Widget 使用BindataFS

import  "github.com/qor/widget"

func main() {
  Widgets := widget.New(&widget.Config{DB: db.DB})
    Widgets.SetAssetFS(assetFS.NameSpace("widgets"))
}

静态文件使用BindataFS

func main() {
  mux := http.NewServeMux()

  // 这将把public下的所有文件添加到一个生成的go文件中,该文件将包含在二进制文件中
  assetFS := assetFS.FileServer(http.Dir("public"))

  // 如果你只想包含指定的路径,你可以像这样使用它
  assetFS := assetFS.FileServer(http.Dir("public"), "javascripts", "stylesheets", "images")

  for _, path := range []string{"javascripts", "stylesheets", "images"} {
    mux.Handle(fmt.Sprintf("/%s/", path), assetFS)
  }
}

I18n

I18n为您的应用程序提供国际化支持,它支持2种存储(后端),数据库和文件系统。

用法

使用存储模式初始化I18n。 您可以同时使用两个存储,较早的存储具有更高的优先级。 因此,在此示例中,I18n将首先在数据库中查找翻译,如果找不到,则继续在YAML文件中查找它。

import (
  "github.com/jinzhu/gorm"
  "github.com/qor/i18n"
  "github.com/qor/i18n/backends/database"
  "github.com/qor/i18n/backends/yaml"
)

func main() {
  db, _ := gorm.Open("mysql", "user:password@/dbname?charset=utf8&parseTime=True&loc=Local")

  I18n := i18n.New(
    database.New(&db), // 从数据库加载翻译
    yaml.New(filepath.Join(config.Root, "config/locales")), // 从YAML文件的config/locales目录中加载翻译
  )

  I18n.T("en-US", "demo.greeting") // 一开始不存在
  I18n.T("en-US", "demo.hello") // 存在于yml文件中
}

一旦为I18n设置了数据库,则在编译应用程序时,I18n.T()中所有未翻译的翻译将被加载到数据库的translations表中。 例如,在示例中我们有一个未翻译的I18n.T(“ en-US”,“ demo.greeting”),因此I18n编译后将在翻译表中生成此记录。

local key value
en-US demo.greeting

YAML文件格式为

en-US:
  demo:
    hello: "Hello, world"

使用内置界面通过QOR Admin进行翻译管理

I18n有一个内置的web翻译界面,它与QOR管理集成在一起。

Admin.AddResource(I18n)

然后会在QOR管理界面添加这样一个页面,参考在线演示

在Golang模板中使用

在模板中使用I18n的简单方法是定义一个t函数并将其注册为FuncMap:

func T(key string, value string, args ...interface{}) string {
  return I18n.Default(value).T("en-US", key, args...)
}

// 然后在模板中使用它
{{ t "demo.greet" "Hello, {{$1}}" "John" }} // -> Hello, John

内置的翻译管理功能

I18n具有直接管理翻译的功能。

// 添加翻译
I18n.AddTranslation(&i18n.Translation{Key: "hello-world", Locale: "en-US", Value: "hello world"})

// 更新翻译
I18n.SaveTranslation(&i18n.Translation{Key: "hello-world", Locale: "en-US", Value: "Hello World"})

// 删除翻译
I18n.DeleteTranslation(&i18n.Translation{Key: "hello-world", Locale: "en-US", Value: "Hello World"})

范围和默认值

调用翻译范围或设置默认值

// 阅读带有Scope的翻译
I18n.Scope("home-page").T("zh-CN", "hello-world") // read translation with translation key `home-page.hello-world`

// 阅读带有“Default Value”的翻译
I18n.Default("Default Value").T("zh-CN", "non-existing-key") // Will return default value `Default Value`

回调

I18n有回调功能。

i18n := New(&backend{})
i18n.AddTranslation(&Translation{Key: "hello-world", Locale: "en-GB", Value: "Hello World"})

fmt.Print(i18n.Fallbacks("en-GB").T("zh-CN", "hello-world")) // "Hello World"

要全局设置fallback Locale),可以使用I18n.FallbackLocales。 此函数接受map[string] []string作为参数。 关键是回退语言环境,[]字符串是可以回退到第一个语言环境的语言环境。

I18n.FallbackLocales = map[string][]string{"en-GB": []{"fr-FR", "de-DE", "zh-CN"}}

插值

I18n使用Golang模板来解析带有插值变量的翻译。

type User struct {
  Name string
}

I18n.AddTranslation(&i18n.Translation{Key: "hello", Locale: "en-US", Value: "Hello {{.Name}}"})

I18n.T("en-US", "hello", User{Name: "Jinzhu"}) //=> Hello Jinzhu

多元化

I18n利用cldr实现多元化,为此目的,它提供了功能p,zero,one,two,few,many,other。 请参考cldr文档以获取更多信息。

I18n.AddTranslation(&i18n.Translation{Key: "count", Locale: "en-US", Value: "{{p "Count" (one "{{.Count}} item") (other "{{.Count}} items")}}"})
I18n.T("en-US", "count", map[string]int{"Count": 1}) //=> 1 item

有序参数

I18n.AddTranslation(&i18n.Translation{Key: "ordered_params", Locale: "en-US", Value: "{{$1}} {{$2}} {{$1}}"})
I18n.T("en-US", "ordered_params", "string1", "string2") //=> string1 string2 string1

内联编辑

在将翻译的数据注册到QOR Admin后,您可以使用QOR Admin界面(UI)管理翻译的数据,不过,我们要提醒你的是,在不了解上下文的情况下翻译一篇译文是非常困难的(而且很容易出错!)。幸运的是, 开发了QOR Admin的内联编辑功能来解决此问题!

内联编辑允许管理员从前端管理翻译。 类似于Golang模板集成,您需要为Golang模板注册功能映射以呈现内联可编辑翻译。

好消息是我们创建了一个包让您轻松执行此操作,它将生成一个FuncMap,您只需要在解析模板时使用它:

// `I18n` hold translations backends
// `en-US` current locale
// `true` enable inline edit mode or not, if inline edit not enabled, it works just like the funcmap in section "Integrate with Golang Templates"
inline_edit.FuncMap(I18n, "en-US", true) // => map[string]interface{}{
                                         //     "t": func(string, ...interface{}) template.HTML {
                                         //        // ...
                                         //      },
                                         //    }

L10n

L10n使您的GORM模型能够针对不同的语言环境进行本地化。

L10n利用GORM回调来处理本地化,因此您需要先注册回调:

import (
  "github.com/jinzhu/gorm"
  "github.com/qor/l10n"
)

func main() {
  db, err := gorm.Open("sqlite3", "demo_db")
  l10n.RegisterCallbacks(&db)
}

模型本地化

将l10n.Locale作为匿名字段嵌入到模型中以启用本地化,例如,在一个以产品管理为重点的假设项目中:

type Product struct {
  gorm.Model
  Name string
  Code string
  l10n.Locale
}

l10n.Locale将使用GORM的AutoMigrate创建字段,将language_code列添加为具有现有主键的复合主键。

language_code列将用于保存本地化模型的语言环境。 如果未设置区域设置,则将使用全局默认区域设置(en-US)。 您可以通过设置l10n.Global来覆盖全局默认语言环境,例如:

l10n.Global = ‘zh-CN’

从全局product创建本地化资源

// 创建全局product
product := Product{Name: "Global product", Description: "Global product description"}
DB.Create(&product)
product.LanguageCode   // "en-US"

// Create zh-CN product
product.Name = "中文产品"
DB.Set("l10n:locale", "zh-CN").Create(&product)

// Query zh-CN product with primary key 111
DB.Set("l10n:locale", "zh-CN").First(&productCN, 111)
productCN.Name         // "中文产品"
productCN.LanguageCode // "zh"

直接创建本地化资源

默认情况下,仅允许创建全局数据,本地数据必须从全局数据本地化。

如果要允许用户直接创建本地化数据,则可以为模型/结构嵌入l10n.LocaleCreatable,例如:

type Product struct {
  gorm.Model
  Name string
  Code string
  l10n.LocaleCreatable
}

保持本地化资源的字段同步

将标签l10n:“sync”添加到您希望始终与全局记录同步的字段中

type Product struct {
  gorm.Model
  Name  string
  Code  string `l10n:"sync"`
  l10n.Locale
}

现在,本地化产品的代码将与全局产品的代码相同。 该代码不受本地化资源的影响,当全局记录更改其代码时,本地化记录的代码将自动同步。

查询方式

L10n提供了5种查询模式。

全局-查找所有全局记录,

语言环境-查找本地化的记录,

反向-查找尚未本地化的全局记录,

不受范围限制-原始查询,查询时不会自动添加语言环境条件,

默认值-查找本地化记录,如果找不到,则返回全局记录。

您可以通过以下方式指定模式:

dbCN := db.Set("l10n:locale", "zh-CN")

mode := "global"
dbCN.Set("l10n:mode", mode).First(&product, 111)
// SELECT * FROM products WHERE id = 111 AND language_code = 'en-US';

mode := "locale"
db.Set("l10n:mode", mode).First(&product, 111)
// SELECT * FROM products WHERE id = 111 AND language_code = 'zh-CN';

Qor集成

尽管L10n可以单独使用,但可以与QOR很好地集成。

默认情况下,QOR仅允许您管理全局语言。 如果已配置身份验证,则QOR管理员将尝试从当前用户获取允许的语言环境。

可查看地区——当前用户具有读权限的地区:

func (user User) ViewableLocales() []string {
  return []string{l10n.Global, "zh-CN", "JP", "EN", "DE"}
}

可编辑区域设置-当前用户对其具有管理(创建/更新/删除)权限的区域设置:

func (user User) EditableLocales() []string {
  if user.role == "global_admin" {
    return []string{l10n.Global, "zh-CN", "EN"}
  } else {
    return []string{"zh-CN", "EN"}
  }
}

渲染

用法

初始化
func main() {
  Render := render.New(&render.Config{
    ViewPaths:     []string{"app/new_view_path"},
    DefaultLayout: "application", // default value is application
    FuncMapMaker:  func(*Render, *http.Request, http.ResponseWriter) template.FuncMap {
      // genereate FuncMap that could be used when render template based on request info
    },
  })
}

接下来,调用Execute函数来渲染模板…

Render.Execute("index", context, request, writer)

Execute函数接受4个参数:

1 模板名称。 在此示例中,Render将从视图路径中查找模板index.tmpl。 默认视图路径为{current_repo_path}/app/views,您可以注册更多视图路径。

2 您可以在模板中使用上下文,它是一个接口{},您可以在视图中使用它。 例如,如果您传递context [“ CurrentUserName”] =“ Memememe”作为上下文。 在模板中,您可以调用{{.CurrentUserName}}来获取值“ Memememe”。

3 http.Request

4 http.ResponseWriter

理解 yield

yield是可以在布局视图中使用的功能,它将呈现当前指定的模板。 对于上面的示例,将yield视为占位符,它将被模板index.tmpl的内容代替。

<!-- app/views/layout/application.tmpl -->
<html>
  <head>
  </head>
  <body>
    {{yield}}
  </body>
</html>
指定布局

默认布局为{current_repo_path}/app/views/layouts/application.tmpl。 如果要使用其他布局,例如new_layout,则可以将其作为参数传递给Layout函数。

Render.Layout("new_layout").Execute("index", context, request, writer)

渲染器将​​在{current_repo_path} /app/views/layouts/new_layout.tmpl中找到布局。

使用辅助功能进行渲染

有时,您可能希望模板中包含一些辅助功能。 渲染器支持通过Funcs函数传递辅助函数。

Render.Funcs(funcsMap).Execute("index", obj, request, writer)

funcsMap基于html/template.FuncMap。 所以用

funcMap := template.FuncMap{
  "Greet": func(name string) string { return "Hello " + name },
}

您可以在模板中调用它

{% raw %}{{Greet "Memememe" }}{% endraw %}
与Responder一起使用

像这样把渲染放在Responder handle函数中。

func handler(writer http.ResponseWriter, request *http.Request) {
  responder.With("html", func() {
    Render.Execute("demo/index", viewContext, *http.Request, http.ResponseWriter)
  }).With([]string{"json", "xml"}, func() {
    writer.Write([]byte("this is a json or xml request"))
  }).Respond(request)
})
与Bindatafs一起使用
$ bindatafs --exit-after-compile=false config/views

func main() {
    Render := render.New()
    Render.SetAssetFS(views.AssetFS)

    Render.Execute("index", context, request, writer)
}

响应器 Responder

注册mime类型

import "github.com/qor/responder"

responder.Register("text/html", "html")
responder.Register("application/json", "json")
responder.Register("application/xml", "xml")

默认情况下,响应者具有上述3种mime类型注册。 您可以使用Register函数注册更多类型,该函数接受2个参数:

mime类型, 类似 text/html
mime类型的格式, 类似 html

响应注册mime类型

func handler(writer http.ResponseWriter, request *http.Request) {
  responder.With("html", func() {
    writer.Write([]byte("this is a html request"))
  }).With([]string{"json", "xml"}, func() {
    writer.Write([]byte("this is a json or xml request"))
  }).Respond(request)
})

如果Responder无法找到相应的mime类型,则示例中的第一个html将是默认的响应类型。


邮件程序

初始化邮件程序

Mailer将支持多个发件人适配器,其工作原理类似,您需要先初始化Mailer,然后使用它来发送电子邮件。

这是使用gomail发送电子邮件的方法

import (
    "github.com/qor/mailer"
    "github.com/qor/mailer/gomailer"
    gomail "gopkg.in/gomail.v2"
)

func main() {
    // 配置 gomail
    dailer := gomail.NewDialer("smtp.example.com", 587, "user", "123456")
    sender, err := dailer.Dial()

    // 初始化邮件程序
    Mailer := mailer.New(&mailer.Config{
        Sender: gomailer.New(&gomailer.Config{Sender: sender}),
    })
}

发送邮件

import "net/mail"

func main() {
    Mailer.Send(mailer.Email{
        TO:          []mail.Address{{Address: "jinzhu@example.org", Name: "jinzhu"}},
        From:        &mail.Address{Address: "jinzhu@example.org"},
        Subject:     "subject",
        Text:        "text email",
        HTML:        "html email <img src='cid:logo.png'/>",
        Attachments: []mailer.Attachment{{FileName: "gomail.go"}, {FileName: "../test/logo.png", Inline: true}},
    })
}

使用模板发送电子邮件

Mailer正在使用Render呈现电子邮件模板和布局,请参考How-To。

电子邮件可以具有HTML和文本版本,在发送电子邮件时,

它将从视图路径中查找模板hello.html.tmpl和布局application.html.tmpl,并将其呈现为HTML版本的内容,并使用模板hello.text.tmpl和布局application.text.tmpl作为文本版本的内容。

如果找不到布局文件,则仅将模板作为内容呈现,如果找不到模板,则将跳过该版本,例如,如果hello.text.tmpl不存在,则我们 只会发送HTML版本。

Mailer.Send(
    mailer.Email{
        TO:      []mail.Address{{Address: Config.DefaultTo}},
        From:    &mail.Address{Address: Config.DefaultFrom},
        Subject: "hello",
    },
    mailer.Template{Name: "hello", Layout: "application", Data: currentUser},
)

邮件视图路径

所有模板和布局应位于app/views/mailers中,但是您可以通过自定义邮件程序的AssetFS来更改或注册更多路径。

import "github.com/qor/assetfs"

func main() {
    assetFS := assetfs.AssetFS().NameSpace("mailer")
    assetFS.RegisterPath("mailers/views")

    Mailer := mailer.New(&mailer.Config{
        Sender: gomailer.New(&gomailer.Config{Sender: sender}),
        AssetFS: assetFS,
    })
}

Sessions

QOR的会话管理

它将其他库(如SCS,Gorilla Session)包装到一个公共接口中,该接口将用于QOR库和您的应用程序。

基本用法

import (
    "github.com/gorilla/sessions"
    "github.com/qor/session/gorilla"
    // "github.com/alexedwards/scs/engine/memstore"
)

var SessionManager = session.ManagerInterface

func main() {
    // Use gorilla session as the backend
    engine := sessions.NewCookieStore([]byte("something-very-secret"))
    SessionManager = gorilla.New("_session", engine)
    // Use SCS as the backend
    // engine := memstore.New(0)
    // SessionManager := scs.New(engine)

    mux := http.NewServeMux()
    mux.HandleFunc("/put", putHandler)
    mux.HandleFunc("/get", getHandler)
    // Your routes

    // Wrap your application's handlers or router with session manager's middleware
    http.ListenAndServe(":7000", manager.Middleware(mux))
}

func putHandler(w http.ResponseWriter, req *http.Request) {
    // Store a key and associated value into session data
    SessionManager.Add(w, req, "key", "value")
}

func getHandler(w http.ResponseWriter, req *http.Request) {
    // Get saved session data with key
    value := SessionManager.Get(req, "key")
    io.WriteString(w, value)
}

会话管理器的接口

type ManagerInterface interface {
    // Add value to session data, if value is not string, will marshal it into JSON encoding and save it into session data.
    Add(w http.ResponseWriter, req *http.Request, key string, value interface{}) error

    // Get value from session data
    Get(req *http.Request, key string) string

    // Pop value from session data
    Pop(w http.ResponseWriter, req *http.Request, key string) string

    // Flash add flash message to session data
    Flash(w http.ResponseWriter, req *http.Request, message Message) error

    // Flashes returns a slice of flash messages from session data
    Flashes(w http.ResponseWriter, req *http.Request) []Message

    // Load get value from session data and unmarshal it into result
    Load(req *http.Request, key string, result interface{}) error

    // PopLoad pop value from session data and unmarshal it into result
    PopLoad(w http.ResponseWriter, req *http.Request, key string, result interface{}) error

    // Middleware returns a new session manager middleware instance.
    Middleware(http.Handler) http.Handler
}

QOR集成

我们在github.com/qor/session/manager包中创建了一个默认的会话管理器,该默认的会话管理器在某些QOR库中使用,例如QOR Admin,默认的QOR Auth来管理会话,闪存消息。

定义如下:

var SessionManager session.ManagerInterface = gorilla.New("_session", sessions.NewCookieStore([]byte("secret")))

您应该将其更改为您自己的会话存储或使用您自己的密码。

import (
    "github.com/qor/session/manager"
)

func main() {
    // Overwrite session manager
    engine := sessions.NewCookieStore([]byte("your-own-secret-code"))
    manager.SessionManager = gorilla.New("_gorilla_session", engine)
}

Worker

Worker在后台运行单个作业,它可以立即执行或在预定时间执行。

一旦向QOR管理员注册,Worker将在导航树中提供一个Workers部分,其中包含列出和管理Worker的以下方面的页面:

所有的工作。
正在运行的作业:当前正在运行的作业。
已计划在未来某个时间运行的作业。
完成:完成工作。
Errors:任何已经运行的Workers输出的任何错误。

可调度作业的管理界面将具有额外的调度时间输入,管理员可以使用该输入设置计划的日期和时间。

用法

import "github.com/qor/worker"

func main() {
  // Define Worker
  Worker := worker.New()

  // Arguments used to run a job
  type sendNewsletterArgument struct {
    Subject      string
    Content      string `sql:"size:65532"`
    SendPassword string

    // If job's argument has `worker.Schedule` embedded, it will get run at a scheduled time
    worker.Schedule
  }

  // Register Job
  Worker.RegisterJob(&worker.Job{
    Name: "Send Newsletter", // Registerd Job Name
    Handler: func(argument interface{}, qorJob worker.QorJobInterface) error {
      // `AddLog` add job log
      qorJob.AddLog("Started sending newsletters...")
      qorJob.AddLog(fmt.Sprintf("Argument: %+v", argument.(*sendNewsletterArgument)))

      for i := 1; i <= 100; i++ {
        time.Sleep(100 * time.Millisecond)
        qorJob.AddLog(fmt.Sprintf("Sending newsletter %v...", i))
        // `SetProgress` set job progress percent, from 0 - 100
        qorJob.SetProgress(uint(i))
      }

      qorJob.AddLog("Finished send newsletters")
      return nil
    },
    // Arguments used to run a job
    Resource: Admin.NewResource(&sendNewsletterArgument{}),
  })

  // Add Worker to qor admin, so you could manage jobs in the admin interface
  Admin.AddResource(Worker)
}

注意事项

如果一个作业在当前时间的2分钟内被调度,那么它将立即运行。
通过管理界面,可以中止当前正在运行的作业
通过管理界面,可以中止计划的作业
通过管理界面,可以更新计划的作业,包括设置新的日期和时间

我是被电商系统这个词吸引来的,但读完官方文档,没觉得与电商系统有啥关系。后台倒是感觉会比较简单,整个搭建也是毕竟快速。

接下来通过示例进行学习,网上它的资料确实少了点。

相关文章