diff --git a/examples/quarkadmin/main.go b/examples/quarkadmin/main.go index 45dcf74..66ae99e 100644 --- a/examples/quarkadmin/main.go +++ b/examples/quarkadmin/main.go @@ -1,9 +1,11 @@ package main import ( - "github.com/quarkcms/quark-go/v2/pkg/app/admin/install" - "github.com/quarkcms/quark-go/v2/pkg/app/admin/middleware" + admininstall "github.com/quarkcms/quark-go/v2/pkg/app/admin/install" + adminmiddleware "github.com/quarkcms/quark-go/v2/pkg/app/admin/middleware" adminservice "github.com/quarkcms/quark-go/v2/pkg/app/admin/service" + miniappinstall "github.com/quarkcms/quark-go/v2/pkg/app/miniapp/install" + miniappmiddleware "github.com/quarkcms/quark-go/v2/pkg/app/miniapp/middleware" miniappservice "github.com/quarkcms/quark-go/v2/pkg/app/miniapp/service" toolservice "github.com/quarkcms/quark-go/v2/pkg/app/tool/service" "github.com/quarkcms/quark-go/v2/pkg/builder" @@ -44,11 +46,17 @@ func main() { // WEB根目录 b.Static("/", "./web/app") - // 自动构建数据库、拉取静态文件 - install.Handle() + // 构建管理后台数据库 + admininstall.Handle() - // 后台中间件 - b.Use(middleware.Handle) + // 管理后台中间件 + b.Use(adminmiddleware.Handle) + + // 构建MiniApp数据库 + miniappinstall.Handle() + + // MiniApp中间件 + b.Use(miniappmiddleware.Handle) // 响应Get请求 b.GET("/", func(ctx *builder.Context) error { diff --git a/examples/sqlitedriver/main.go b/examples/sqlitedriver/main.go index ea01c51..bf47e77 100644 --- a/examples/sqlitedriver/main.go +++ b/examples/sqlitedriver/main.go @@ -4,7 +4,6 @@ import ( "github.com/quarkcms/quark-go/v2/pkg/app/admin/install" "github.com/quarkcms/quark-go/v2/pkg/app/admin/middleware" adminservice "github.com/quarkcms/quark-go/v2/pkg/app/admin/service" - mixservice "github.com/quarkcms/quark-go/v2/pkg/app/mix/service" toolservice "github.com/quarkcms/quark-go/v2/pkg/app/tool/service" "github.com/quarkcms/quark-go/v2/pkg/builder" "gorm.io/driver/sqlite" @@ -22,9 +21,6 @@ func main() { // 加载后台服务 providers = append(providers, adminservice.Providers...) - // 加载Mix服务 - providers = append(providers, mixservice.Providers...) - // 加载工具服务 providers = append(providers, toolservice.Providers...) diff --git a/pkg/app/admin/service/providers.go b/pkg/app/admin/service/providers.go index bf4baae..758938d 100644 --- a/pkg/app/admin/service/providers.go +++ b/pkg/app/admin/service/providers.go @@ -13,6 +13,7 @@ var Providers = []interface{}{ &logins.Index{}, &layouts.Index{}, &dashboards.Index{}, + &resources.User{}, &resources.Admin{}, &resources.Role{}, &resources.Permission{}, diff --git a/pkg/app/admin/service/resources/user.go b/pkg/app/admin/service/resources/user.go new file mode 100644 index 0000000..6eb2481 --- /dev/null +++ b/pkg/app/admin/service/resources/user.go @@ -0,0 +1,192 @@ +package resources + +import ( + "strconv" + "time" + + "github.com/quarkcms/quark-go/v2/pkg/app/admin/component/form/fields/radio" + "github.com/quarkcms/quark-go/v2/pkg/app/admin/component/form/rule" + "github.com/quarkcms/quark-go/v2/pkg/app/admin/service/actions" + "github.com/quarkcms/quark-go/v2/pkg/app/admin/service/searches" + "github.com/quarkcms/quark-go/v2/pkg/app/admin/template/resource" + "github.com/quarkcms/quark-go/v2/pkg/app/miniapp/model" + "github.com/quarkcms/quark-go/v2/pkg/builder" + "github.com/quarkcms/quark-go/v2/pkg/utils/hash" +) + +type User struct { + resource.Template +} + +// 初始化 +func (p *User) Init(ctx *builder.Context) interface{} { + + // 标题 + p.Title = "用户" + + // 模型 + p.Model = &model.User{} + + // 分页 + p.PerPage = 10 + + // 是否具有导出功能 + p.WithExport = true + + return p +} + +// 字段 +func (p *User) Fields(ctx *builder.Context) []interface{} { + field := &resource.Field{} + + return []interface{}{ + field.ID("id", "ID"), + + field.Image("avatar", "头像").OnlyOnForms(), + + field.Text("username", "用户名", func() interface{} { + + return "" + p.Field["username"].(string) + "" + }). + SetRules([]*rule.Rule{ + rule.Required(true, "用户名必须填写"), + rule.Min(6, "用户名不能少于6个字符"), + rule.Max(20, "用户名不能超过20个字符"), + }). + SetCreationRules([]*rule.Rule{ + rule.Unique("users", "username", "用户名已存在"), + }). + SetUpdateRules([]*rule.Rule{ + rule.Unique("users", "username", "{id}", "用户名已存在"), + }), + + field.Text("nickname", "昵称"). + SetEditable(true). + SetRules([]*rule.Rule{ + rule.Required(true, "昵称必须填写"), + }), + + field.Text("email", "邮箱"). + SetRules([]*rule.Rule{ + rule.Required(true, "邮箱必须填写"), + rule.Email("邮箱格式错误"), + }). + SetCreationRules([]*rule.Rule{ + rule.Unique("users", "email", "邮箱已存在"), + }). + SetUpdateRules([]*rule.Rule{ + rule.Unique("users", "email", "{id}", "邮箱已存在"), + }), + + field.Text("phone", "手机号"). + SetRules([]*rule.Rule{ + rule.Required(true, "手机号必须填写"), + rule.Phone("手机号格式错误"), + }). + SetCreationRules([]*rule.Rule{ + rule.Unique("users", "phone", "手机号已存在"), + }). + SetUpdateRules([]*rule.Rule{ + rule.Unique("users", "phone", "{id}", "手机号已存在"), + }), + + field.Radio("sex", "性别"). + SetRules([]*rule.Rule{ + rule.Required(true, "请选择性别"), + }). + SetOptions([]*radio.Option{ + { + Value: 1, + Label: "男", + }, + { + Value: 2, + Label: "女", + }, + }). + SetFilters(true). + SetDefault(1), + + field.Password("password", "密码"). + SetCreationRules([]*rule.Rule{ + rule.Required(true, "密码必须填写"), + }). + OnlyOnForms(). + ShowOnImporting(true), + + field.Datetime("last_login_time", "最后登录时间", func() interface{} { + if p.Field["last_login_time"] == nil { + return p.Field["last_login_time"] + } + + if p.Field["last_login_time"].(time.Time).Format("2006-01-02 15:04:05") == "0001-01-01 00:00:00" { + return nil + } + + return p.Field["last_login_time"].(time.Time).Format("2006-01-02 15:04:05") + }).OnlyOnIndex(), + + field.Switch("status", "状态"). + SetRules([]*rule.Rule{ + rule.Required(true, "请选择状态"), + }). + SetTrueValue("正常"). + SetFalseValue("禁用"). + SetEditable(true). + SetDefault(true), + } +} + +// 搜索 +func (p *User) Searches(ctx *builder.Context) []interface{} { + + return []interface{}{ + searches.Input("username", "用户名"), + searches.Input("nickname", "昵称"), + searches.Status(), + searches.DatetimeRange("last_login_time", "登录时间"), + } +} + +// 行为 +func (p *User) Actions(ctx *builder.Context) []interface{} { + + return []interface{}{ + actions.Import(), + actions.CreateLink(), + actions.BatchDelete(), + actions.BatchDisable(), + actions.BatchEnable(), + actions.DetailLink(), + actions.More(). + SetActions([]interface{}{ + actions.EditLink(), + actions.Delete(), + }), + actions.FormSubmit(), + actions.FormReset(), + actions.FormBack(), + actions.FormExtraBack(), + } +} + +// 编辑页面显示前回调 +func (p *User) BeforeEditing(ctx *builder.Context, data map[string]interface{}) map[string]interface{} { + + // 编辑页面清理password + delete(data, "password") + + return data +} + +// 保存数据前回调 +func (p *User) BeforeSaving(ctx *builder.Context, submitData map[string]interface{}) (map[string]interface{}, error) { + + // 加密密码 + if submitData["password"] != nil { + submitData["password"] = hash.Make(submitData["password"].(string)) + } + + return submitData, nil +} diff --git a/pkg/app/miniapp/install/install.go b/pkg/app/miniapp/install/install.go new file mode 100644 index 0000000..0834cef --- /dev/null +++ b/pkg/app/miniapp/install/install.go @@ -0,0 +1,26 @@ +package install + +import ( + "github.com/quarkcms/quark-go/v2/pkg/app/miniapp/model" + "github.com/quarkcms/quark-go/v2/pkg/dal/db" + "gorm.io/gorm" +) + +// 执行安装操作 +func Handle() { + + // 迁移数据 + db.Client.AutoMigrate( + &model.User{}, + ) + + // 如果用户不存在,初始化数据库数据 + userInfo, err := (&model.User{}).GetInfoById(1) + if err != nil && err != gorm.ErrRecordNotFound { + panic(err) + } + if userInfo.Id == 0 { + // 数据填充 + (&model.User{}).Seeder() + } +} diff --git a/pkg/app/miniapp/middleware/middleware.go b/pkg/app/miniapp/middleware/middleware.go new file mode 100644 index 0000000..1157037 --- /dev/null +++ b/pkg/app/miniapp/middleware/middleware.go @@ -0,0 +1,30 @@ +package middleware + +import ( + "strings" + + "github.com/quarkcms/quark-go/v2/pkg/app/miniapp/model" + "github.com/quarkcms/quark-go/v2/pkg/builder" +) + +// 中间件 +func Handle(ctx *builder.Context) error { + + // 排除非后台路由 + if !strings.Contains(ctx.Path(), "api/miniapp/user") { + return ctx.Next() + } + + // 获取登录信息 + userInfo, err := (&model.User{}).GetAuthUser(ctx.Engine.GetConfig().AppKey, ctx.Token()) + if err != nil { + return ctx.JSON(401, builder.Error(err.Error())) + } + + guardName := userInfo.GuardName + if guardName != "user" { + return ctx.JSON(401, builder.Error("401 Unauthozied")) + } + + return ctx.Next() +} diff --git a/pkg/app/miniapp/model/user.go b/pkg/app/miniapp/model/user.go new file mode 100644 index 0000000..dda70b5 --- /dev/null +++ b/pkg/app/miniapp/model/user.go @@ -0,0 +1,145 @@ +package model + +import ( + "errors" + "time" + + "github.com/golang-jwt/jwt/v4" + adminmodel "github.com/quarkcms/quark-go/v2/pkg/app/admin/model" + "github.com/quarkcms/quark-go/v2/pkg/dal/db" + "github.com/quarkcms/quark-go/v2/pkg/utils/hash" + "gorm.io/gorm" +) + +// 字段 +type User struct { + Id int `json:"id" gorm:"autoIncrement"` + Username string `json:"username" gorm:"size:20;index:Users_username_unique,unique;not null"` + Nickname string `json:"nickname" gorm:"size:200;not null"` + Sex int `json:"sex" gorm:"size:4;not null;default:1"` + Email string `json:"email" gorm:"size:50;index:users_email_unique,unique;not null"` + Phone string `json:"phone" gorm:"size:11;index:users_phone_unique,unique;not null"` + Password string `json:"password" gorm:"size:255;not null"` + Avatar string `json:"avatar" gorm:"size:1000"` + LastLoginIp string `json:"last_login_ip" gorm:"size:255"` + LastLoginTime time.Time `json:"last_login_time"` + WxOpenid string `json:"wx_openid" gorm:"size:255"` + WxUnionid string `json:"wx_unionid" gorm:"size:255"` + Status int `json:"status" gorm:"size:1;not null;default:1"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `json:"deleted_at"` +} + +// 用户JWT结构体 +type UserClaims struct { + Id int `json:"id"` + Username string `json:"username"` + Nickname string `json:"nickname"` + Sex int `json:"sex"` + Email string `json:"email"` + Phone string `json:"phone"` + Avatar string `json:"avatar"` + GuardName string `json:"guard_name"` + jwt.RegisteredClaims +} + +// 用户Seeder +func (model *User) Seeder() { + + // 如果菜单已存在,不执行Seeder操作 + if (&adminmodel.Menu{}).IsExist(18) { + return + } + + // 创建菜单 + menuSeeders := []*adminmodel.Menu{ + {Id: 18, Name: "用户管理", GuardName: "admin", Icon: "icon-user", Type: 1, Pid: 0, Sort: 0, Path: "/user", Show: 1, IsEngine: 0, IsLink: 0, Status: 1}, + {Id: 19, Name: "用户列表", GuardName: "admin", Icon: "", Type: 2, Pid: 18, Sort: 0, Path: "/api/admin/user/index", Show: 1, IsEngine: 1, IsLink: 0, Status: 1}, + } + db.Client.Create(&menuSeeders) + + seeders := []User{ + {Username: "tangtanglove", Nickname: "默认用户", Email: "tangtanglove@yourweb.com", Phone: "10086", Password: hash.Make("123456"), Sex: 1, Status: 1, LastLoginTime: time.Now()}, + } + + db.Client.Create(&seeders) +} + +// 获取用户JWT信息 +func (model *User) GetClaims(UserInfo *User) (userClaims *UserClaims) { + userClaims = &UserClaims{ + UserInfo.Id, + UserInfo.Username, + UserInfo.Nickname, + UserInfo.Sex, + UserInfo.Email, + UserInfo.Phone, + UserInfo.Avatar, + "user", + jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)), // 过期时间,默认24小时 + IssuedAt: jwt.NewNumericDate(time.Now()), // 颁发时间 + NotBefore: jwt.NewNumericDate(time.Now()), // 不早于时间 + Issuer: "QuarkGo", // 颁发人 + Subject: "User Token", // 主题信息 + }, + } + + return userClaims +} + +// 获取当前认证的用户信息,默认参数为tokenString +func (model *User) GetAuthUser(appKey string, tokenString string) (userClaims *UserClaims, Error error) { + token, err := jwt.ParseWithClaims(tokenString, &UserClaims{}, func(token *jwt.Token) (interface{}, error) { + return []byte(appKey), nil + }) + if err != nil { + if ve, ok := err.(*jwt.ValidationError); ok { + if ve.Errors&jwt.ValidationErrorMalformed != 0 { + return nil, errors.New("token格式错误") + } else if ve.Errors&jwt.ValidationErrorExpired != 0 { + return nil, errors.New("token已过期") + } else if ve.Errors&jwt.ValidationErrorNotValidYet != 0 { + return nil, errors.New("token未生效") + } else { + return nil, err + } + } + } + + if claims, ok := token.Claims.(*UserClaims); ok && token.Valid { + return claims, nil + } + + return nil, errors.New("token不可用") +} + +// 通过ID获取用户信息 +func (model *User) GetInfoById(id interface{}) (User *User, Error error) { + err := db.Client.Where("status = ?", 1).Where("id = ?", id).First(&User).Error + + return User, err +} + +// 通过用户名获取用户信息 +func (model *User) GetInfoByUsername(username string) (User *User, Error error) { + err := db.Client.Where("status = ?", 1).Where("username = ?", username).First(&User).Error + if User.Avatar != "" { + User.Avatar = (&adminmodel.Picture{}).GetPath(User.Avatar) // 获取头像地址 + } + + return User, err +} + +// 更新最后一次登录数据 +func (model *User) UpdateLastLogin(uid int, lastLoginIp string, lastLoginTime time.Time) error { + data := User{ + LastLoginIp: lastLoginIp, + LastLoginTime: lastLoginTime, + } + + return db.Client. + Where("id = ?", uid). + Updates(&data).Error +} diff --git a/pkg/app/miniapp/service/pages/my.go b/pkg/app/miniapp/service/pages/my.go index 7794a7e..a328096 100644 --- a/pkg/app/miniapp/service/pages/my.go +++ b/pkg/app/miniapp/service/pages/my.go @@ -1,7 +1,6 @@ package pages import ( - "github.com/quarkcms/quark-go/v2/pkg/app/miniapp/component/navbar" "github.com/quarkcms/quark-go/v2/pkg/app/miniapp/template/page" "github.com/quarkcms/quark-go/v2/pkg/builder" ) @@ -15,11 +14,6 @@ func (p *My) Init(ctx *builder.Context) interface{} { return p } -// 头部导航 -func (p *My) Navbar(ctx *builder.Context, navbar *navbar.Component) interface{} { - return navbar.SetTitle("我的") -} - // 组件渲染 func (p *My) Content(ctx *builder.Context) interface{} { return "我的" diff --git a/pkg/app/miniapp/template/login/login.go b/pkg/app/miniapp/template/login/login.go new file mode 100644 index 0000000..8bfad1a --- /dev/null +++ b/pkg/app/miniapp/template/login/login.go @@ -0,0 +1,50 @@ +package login + +import ( + "github.com/quarkcms/quark-go/v2/pkg/app/miniapp/template/page" + "github.com/quarkcms/quark-go/v2/pkg/builder" + "github.com/quarkcms/quark-go/v2/pkg/dal/db" +) + +// 后台登录模板 +type Template struct { + page.Template + FromStyle string + Api string +} + +// 初始化 +func (p *Template) Init(ctx *builder.Context) interface{} { + return p +} + +// 初始化模板 +func (p *Template) TemplateInit(ctx *builder.Context) interface{} { + + // 初始化数据对象 + p.DB = db.Client + + // 标题 + p.Title = "登录" + + return p +} + +// 初始化路由映射 +func (p *Template) RouteInit() interface{} { + p.GET("/api/miniapp/login/:resource/index", p.Render) // 渲染登录页面路由 + p.POST("/api/miniapp/login/:resource/handle", p.Handle) // 后台登录执行路由 + + return p +} + +// 内容 +func (p *Template) Content(ctx *builder.Context) interface{} { + + return "登录页面" +} + +// 执行表单 +func (p *Template) Handle(ctx *builder.Context) error { + return ctx.JSONError("请自行处理表单逻辑") +}