diff --git a/README.md b/README.md index 3782f59..d4e5c95 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ -[![Build Status](https://travis-ci.org/ouqiang/gocron.png)](https://travis-ci.org/ouqiang/gocron) # gocron - 定时任务管理系统 +[![Build Status](https://travis-ci.org/ouqiang/gocron.png)](https://travis-ci.org/ouqiang/gocron) +[![Downloads](https://img.shields.io/github/downloads/ouqiang/gocron/total.svg)](https://github.com/ouqiang/gocron/releases) +[![license](https://img.shields.io/github/license/mashape/apistatus.svg?maxAge=2592000)](https://github.com/ouqiang/gocron/blob/master/LICENSE) +[![Release](https://img.shields.io/github/release/ouqiang/gocron.svg?label=Release)](https://github.com/ouqiang/gocron/releases) # 项目简介 使用Go语言开发的定时任务集中调度和管理系统, 用于替代Linux-crontab [查看文档](https://github.com/ouqiang/gocron/wiki) @@ -33,7 +36,7 @@ ## 下载 -[v1.2.2](https://github.com/ouqiang/gocron/releases/tag/v1.2.2) +[releases](https://github.com/ouqiang/gocron/releases) [版本升级](https://github.com/ouqiang/gocron/wiki/版本升级) @@ -84,9 +87,8 @@ - [x] 批量开启、关闭、删除任务 - [x] 调度器与任务节点通信支持https - [x] 任务分组 -- [ ] 多用户 +- [x] 多用户 - [ ] 权限控制 -- [ ] 新增任务API接口 ## 程序使用的组件 * Web框架 [Macaron](http://go-macaron.com/) diff --git a/build.sh b/build.sh index 045ea07..58aada1 100644 --- a/build.sh +++ b/build.sh @@ -16,6 +16,8 @@ OS='' ARCH='' # 应用名称 APP_NAME='gocron' +# 版本号 +VERSION='' # 可执行文件名 EXEC_NAME='' # 压缩包名称 @@ -23,13 +25,15 @@ COMPRESS_FILE='' # -p 平台 -a 架构 -while getopts "p:a:" OPT; +while getopts "p:a:v:" OPT; do case $OPT in p) OS=$OPTARG ;; a) ARCH=$OPTARG ;; + v) VERSION=$OPTARG + ;; esac done @@ -52,6 +56,11 @@ if [[ $ARCH != '386' && $ARCH != 'amd64' ]];then exit 1 fi +if [[ -z $VERSION ]];then + echo '版本号不能为空' + exit 1 +fi + echo '开始编译调度器' if [[ $OS = 'windows' ]];then GOOS=$OS GOARCH=$ARCH go build -tags gocron -ldflags '-w' @@ -66,10 +75,10 @@ echo '编译完成' if [[ $OS = 'windows' ]];then EXEC_NAME=${APP_NAME}.exe - COMPRESS_FILE=${APP_NAME}-${OS}-${ARCH}.zip + COMPRESS_FILE=${APP_NAME}-v${VERSION}-${OS}-${ARCH}.zip else EXEC_NAME=${APP_NAME} - COMPRESS_FILE=${APP_NAME}-${OS}-${ARCH}.tar.gz + COMPRESS_FILE=${APP_NAME}-v${VERSION}-${OS}-${ARCH}.tar.gz fi mkdir -p $TEMP_DIR/$APP_NAME diff --git a/build_node.sh b/build_node.sh index dd3d31a..1f4c3a2 100644 --- a/build_node.sh +++ b/build_node.sh @@ -14,6 +14,8 @@ OS='' ARCH='' # 应用名称 APP_NAME='gocron-node' +# 版本号 +VERSION='' # 可执行文件名 EXEC_NAME='' # 压缩包名称 @@ -21,13 +23,15 @@ COMPRESS_FILE='' # -p 平台 -a 架构 -while getopts "p:a:" OPT; +while getopts "p:a:v:" OPT; do case $OPT in p) OS=$OPTARG ;; a) ARCH=$OPTARG ;; + v) VERSION=$OPTARG + ;; esac done @@ -50,12 +54,17 @@ if [[ $ARCH != '386' && $ARCH != 'amd64' ]];then exit 1 fi +if [[ -z $VERSION ]];then + echo '版本号不能为空' + exit 1 +fi + if [[ $OS = 'windows' ]];then EXEC_NAME=${APP_NAME}.exe - COMPRESS_FILE=${APP_NAME}-${OS}-${ARCH}.zip + COMPRESS_FILE=${APP_NAME}-v${VERSION}-${OS}-${ARCH}.zip else EXEC_NAME=${APP_NAME} - COMPRESS_FILE=${APP_NAME}-${OS}-${ARCH}.tar.gz + COMPRESS_FILE=${APP_NAME}-v${VERSION}-${OS}-${ARCH}.tar.gz fi echo '开始编译任务节点' diff --git a/gocron.go b/gocron.go index 086797d..51f1640 100644 --- a/gocron.go +++ b/gocron.go @@ -10,7 +10,7 @@ import ( "github.com/ouqiang/gocron/cmd" ) -const AppVersion = "1.2.2" +const AppVersion = "1.3" func main() { app := cli.NewApp() diff --git a/models/migration.go b/models/migration.go index 96b75e2..ba00ed0 100644 --- a/models/migration.go +++ b/models/migration.go @@ -53,10 +53,11 @@ func (migration *Migration) Upgrade(oldVersionId int) { return } - versionIds := []int{110, 122} + versionIds := []int{110, 122, 130} upgradeFuncs := []func(*xorm.Session) error{ migration.upgradeFor110, migration.upgradeFor122, + migration.upgradeFor130, } startIndex := -1 @@ -155,3 +156,16 @@ func (migration *Migration) upgradeFor122(session *xorm.Session) error { return err } + +// 升级到1.2.3版本 +func (migration *Migration) upgradeFor130(session *xorm.Session) error { + logger.Info("开始升级到v1.3") + + tableName := TablePrefix + "user" + // 删除user表deleted字段 + _, err := session.Exec(fmt.Sprintf("ALTER TABLE %s DROP COLUMN deleted", tableName)) + + logger.Info("已升级到v1.3\n") + + return err +} \ No newline at end of file diff --git a/models/model.go b/models/model.go index 4b18b5d..81195b9 100644 --- a/models/model.go +++ b/models/model.go @@ -26,7 +26,6 @@ const ( Running Status = 1 // 运行中 Finish Status = 2 // 完成 Cancel Status = 3 // 取消 - Waiting Status = 5 // 等待中 ) const ( diff --git a/models/user.go b/models/user.go index 00f8703..dd2fe2d 100644 --- a/models/user.go +++ b/models/user.go @@ -16,7 +16,6 @@ type User struct { Email string `xorm:"varchar(50) notnull unique default '' "` // 邮箱 Created time.Time `xorm:"datetime notnull created"` Updated time.Time `xorm:"datetime updated"` - Deleted time.Time `xorm:"datetime deleted"` IsAdmin int8 `xorm:"tinyint notnull default 0"` // 是否是管理员 1:管理员 0:普通用户 Status Status `xorm:"tinyint notnull default 1"` // 1: 正常 0:禁用 BaseModel `xorm:"-"` @@ -65,8 +64,8 @@ func (user *User) Enable(id int) (int64, error) { // 验证用户名和密码 func (user *User) Match(username, password string) bool { - where := "(name = ? OR email = ?)" - _, err := Db.Where(where, username, username).Get(user) + where := "(name = ? OR email = ?) AND status =? " + _, err := Db.Where(where, username, username, Enabled).Get(user) if err != nil { return false } @@ -78,19 +77,35 @@ func (user *User) Match(username, password string) bool { return true } +// 获取用户详情 +func (user *User) Find(id int) (error) { + _, err := Db.Id(id).Get(user) + + return err +} + // 用户名是否存在 -func (user *User) UsernameExists(username string) (int64, error) { +func (user *User) UsernameExists(username string, uid int) (int64, error) { + if uid > 0 { + return Db.Where("name = ? AND id != ?", username, uid).Count(user) + } + return Db.Where("name = ?", username).Count(user) } // 邮箱地址是否存在 -func (user *User) EmailExists(email string) (int64, error) { +func (user *User) EmailExists(email string, uid int) (int64, error) { + if uid > 0 { + return Db.Where("email = ? AND id != ?", email, uid).Count(user) + } + return Db.Where("email = ?", email).Count(user) } -func (user *User) List() ([]User, error) { +func (user *User) List(params CommonMap) ([]User, error) { + user.parsePageAndPageSize(params) list := make([]User, 0) - err := Db.Desc("id").Find(&list) + err := Db.Desc("id").Limit(user.PageSize, user.pageLimitOffset()).Find(&list) return list, err } diff --git a/routers/base/base.go b/routers/base/base.go index 11a412d..acc8175 100644 --- a/routers/base/base.go +++ b/routers/base/base.go @@ -5,6 +5,7 @@ import ( "gopkg.in/macaron.v1" ) +// ParsePageAndPageSize 解析查询参数中的页数和每页数量 func ParsePageAndPageSize(ctx *macaron.Context, params models.CommonMap) { page := ctx.QueryInt("page") pageSize := ctx.QueryInt("page_size") diff --git a/routers/host/host.go b/routers/host/host.go index ce8fa9a..2747bd1 100644 --- a/routers/host/host.go +++ b/routers/host/host.go @@ -18,6 +18,7 @@ import ( "strings" ) +// Index 主机列表 func Index(ctx *macaron.Context) { hostModel := new(models.Host) queryParams := parseQueryParams(ctx) @@ -42,11 +43,13 @@ func Index(ctx *macaron.Context) { ctx.HTML(200, "host/index") } +// Create 创建主机页面 func Create(ctx *macaron.Context) { ctx.Data["Title"] = "添加主机" ctx.HTML(200, "host/host_form") } +// Edit 修改主机页面 func Edit(ctx *macaron.Context) { ctx.Data["Title"] = "编辑主机" hostModel := new(models.Host) @@ -67,6 +70,7 @@ type HostForm struct { Remark string } +// Error 表单验证错误处理 func (f HostForm) Error(ctx *macaron.Context, errs binding.Errors) { if len(errs) == 0 { return @@ -77,6 +81,7 @@ func (f HostForm) Error(ctx *macaron.Context, errs binding.Errors) { ctx.Resp.Write([]byte(content)) } +// Store 保存、修改主机信息 func Store(ctx *macaron.Context, form HostForm) string { json := utils.JsonResponse{} hostModel := new(models.Host) @@ -129,6 +134,7 @@ func Store(ctx *macaron.Context, form HostForm) string { return json.Success("保存成功", nil) } +// Remove 删除主机 func Remove(ctx *macaron.Context) string { id, err := strconv.Atoi(ctx.Params(":id")) json := utils.JsonResponse{} @@ -161,6 +167,7 @@ func Remove(ctx *macaron.Context) string { return json.Success("操作成功", nil) } +// Ping 测试主机是否可连接 func Ping(ctx *macaron.Context) string { id := ctx.ParamsInt(":id") hostModel := new(models.Host) diff --git a/routers/routers.go b/routers/routers.go index 1238dc5..a27eff9 100644 --- a/routers/routers.go +++ b/routers/routers.go @@ -41,11 +41,20 @@ func Register(m *macaron.Macaron) { // 用户 m.Group("/user", func() { + m.Get("", user.Index) + m.Get("/create", user.Create) + m.Get("/edit/:id", user.Edit) + m.Post("/store", binding.Bind(user.UserForm{}), user.Store) + m.Post("/remove/:id", user.Remove) m.Get("/login", user.Login) m.Post("/login", user.ValidateLogin) m.Get("/logout", user.Logout) - m.Get("/editPassword", user.EditPassword) - m.Post("/editPassword", user.UpdatePassword) + m.Post("/enable/:id", user.Enable) + m.Post("/disable/:id", user.Disable) + m.Get("/editMyPassword", user.EditMyPassword) + m.Post("/editMyPassword", user.UpdateMyPassword) + m.Get("/editPassword/:id", user.EditPassword) + m.Post("/editPassword/:id", user.UpdatePassword) }) // 定时任务 diff --git a/routers/task/task.go b/routers/task/task.go index f8f6cc3..eb4c7d8 100644 --- a/routers/task/task.go +++ b/routers/task/task.go @@ -208,7 +208,7 @@ func Store(ctx *macaron.Context, form TaskForm) string { taskHostModel.Remove(id) } - status, err := taskModel.GetStatus(id) + status, _ := taskModel.GetStatus(id) if status == models.Enabled && taskModel.Level == models.TaskLevelParent { addTaskToTimer(id) } diff --git a/routers/user/user.go b/routers/user/user.go index 5aa89aa..05e7ca2 100644 --- a/routers/user/user.go +++ b/routers/user/user.go @@ -6,23 +6,222 @@ import ( "github.com/ouqiang/gocron/models" "github.com/ouqiang/gocron/modules/logger" "github.com/ouqiang/gocron/modules/utils" + "github.com/ouqiang/gocron/routers/base" "gopkg.in/macaron.v1" + "github.com/Unknwon/paginater" + "html/template" + "fmt" + "strings" ) // @author qiang.ou // @date 2017/4/23-14:16 +// UserForm 用户表单 +type UserForm struct { + Id int + Name string `binding:"Required;MaxSize(32)"` // 用户名 + Password string // 密码 + ConfirmPassword string // 确认密码 + Email string `binding:"Required;MaxSize(50)"` // 邮箱 + IsAdmin int8 // 是否是管理员 1:管理员 0:普通用户 + Status models.Status +} + +// Index 用户列表页 +func Index(ctx *macaron.Context) { + queryParams := parseQueryParams(ctx) + userModel := new(models.User) + users, err := userModel.List(queryParams) + if err != nil { + logger.Error(err) + } + total, err := userModel.Total() + if err != nil { + logger.Error(err) + } + PageParams := fmt.Sprintf("page_size=%d", queryParams["PageSize"]) + queryParams["PageParams"] = template.URL(PageParams) + p := paginater.New(int(total), queryParams["PageSize"].(int), queryParams["Page"].(int), 5) + ctx.Data["Pagination"] = p + ctx.Data["Params"] = queryParams + ctx.Data["Title"] = "用户列表" + ctx.Data["Users"] = users + ctx.HTML(200, "user/index") +} + +// 解析查询参数 +func parseQueryParams(ctx *macaron.Context) models.CommonMap { + var params models.CommonMap = models.CommonMap{} + base.ParsePageAndPageSize(ctx, params) + + return params +} + +// Create 新增用户页 +func Create(ctx *macaron.Context) { + userModel := new(models.User) + userModel.Status = models.Enabled + userModel.IsAdmin = 0 + ctx.Data["User"] = userModel + ctx.Data["Title"] = "添加用户" + ctx.HTML(200, "user/user_form") +} + +// 编辑页面 +func Edit(ctx *macaron.Context) { + ctx.Data["Title"] = "编辑用户" + userModel := new(models.User) + id := ctx.ParamsInt(":id") + err := userModel.Find(id) + if err != nil { + logger.Error(err) + } + ctx.Data["User"] = userModel + ctx.HTML(200, "user/user_form") +} + +// 保存任务 +func Store(ctx *macaron.Context, form UserForm) string { + form.Name = strings.TrimSpace(form.Name) + form.Email = strings.TrimSpace(form.Email) + form.Password = strings.TrimSpace(form.Password) + form.ConfirmPassword = strings.TrimSpace(form.ConfirmPassword) + json := utils.JsonResponse{} + userModel := models.User{} + nameExists, err := userModel.UsernameExists(form.Name, form.Id) + if err != nil { + return json.CommonFailure(utils.FailureContent, err) + } + if nameExists > 0 { + return json.CommonFailure("用户名已存在") + } + + emailExists, err := userModel.EmailExists(form.Email, form.Id) + if err != nil { + return json.CommonFailure(utils.FailureContent, err) + } + if emailExists > 0 { + return json.CommonFailure("邮箱已存在") + } + + if form.Id == 0 { + if form.Password == "" { + return json.CommonFailure("请输入密码") + } + if form.ConfirmPassword == "" { + return json.CommonFailure("请再次输入密码") + } + if form.Password != form.ConfirmPassword { + return json.CommonFailure("两次密码输入不一致") + } + } + userModel.Name = form.Name + userModel.Email = form.Email + userModel.Password = form.Password + userModel.IsAdmin = form.IsAdmin + userModel.Status = form.Status + + if form.Id == 0 { + _, err = userModel.Create() + if err != nil { + return json.CommonFailure("添加失败", err) + } + } else { + _, err = userModel.Update(form.Id, models.CommonMap{ + "name": form.Name, + "email": form.Email, + "status": form.Status, + "is_admin": form.IsAdmin, + }) + if err != nil { + return json.CommonFailure("修改失败", err) + } + } + + + return json.Success("保存成功", nil) +} + +// 删除用户 +func Remove(ctx *macaron.Context) string { + id := ctx.ParamsInt(":id") + json := utils.JsonResponse{} + + userModel := new(models.User) + _, err := userModel.Delete(id) + if err != nil { + return json.CommonFailure(utils.FailureContent, err) + } + + return json.Success(utils.SuccessContent, nil) +} + +// 激活用户 +func Enable(ctx *macaron.Context) string { + return changeStatus(ctx, models.Enabled) +} + +// 禁用用户 +func Disable(ctx *macaron.Context) string { + return changeStatus(ctx, models.Disabled) +} + +// 改变任务状态 +func changeStatus(ctx *macaron.Context, status models.Status) string { + id := ctx.ParamsInt(":id") + json := utils.JsonResponse{} + userModel := new(models.User) + _, err := userModel.Update(id, models.CommonMap{ + "Status": status, + }) + if err != nil { + return json.CommonFailure(utils.FailureContent, err) + } + + return json.Success(utils.SuccessContent, nil) +} + +// Login 用户登录 func Login(ctx *macaron.Context) { ctx.Data["Title"] = "用户登录" ctx.HTML(200, "user/login") } +// EditPassword 修改密码页面 func EditPassword(ctx *macaron.Context) { + id := ctx.ParamsInt(":id") ctx.Data["Title"] = "修改密码" + ctx.Data["Id"] = id ctx.HTML(200, "user/editPassword") } -func UpdatePassword(ctx *macaron.Context, sess session.Store) string { +// UpdatePassword 更新我的密码 +func UpdatePassword(ctx *macaron.Context) string { + id := ctx.ParamsInt(":id") + newPassword := ctx.QueryTrim("new_password") + confirmNewPassword := ctx.QueryTrim("confirm_new_password") + json := utils.JsonResponse{} + if newPassword == "" || confirmNewPassword == "" { + return json.CommonFailure("请输入密码") + } + userModel := new(models.User) + _, err := userModel.UpdatePassword(id, newPassword) + if err != nil { + return json.CommonFailure("修改失败") + } + + return json.Success("修改成功", nil) +} + +// EditMyPassword 修改我的密码页面 +func EditMyPassword(ctx *macaron.Context) { + ctx.Data["Title"] = "修改密码" + ctx.HTML(200, "user/editMyPassword") +} + +// UpdateMyPassword 更新我的密码 +func UpdateMyPassword(ctx *macaron.Context, sess session.Store) string { oldPassword := ctx.QueryTrim("old_password") newPassword := ctx.QueryTrim("new_password") confirmNewPassword := ctx.QueryTrim("confirm_new_password") @@ -48,6 +247,7 @@ func UpdatePassword(ctx *macaron.Context, sess session.Store) string { return json.Success("修改成功", nil) } +// ValidateLogin 验证用户登录 func ValidateLogin(ctx *macaron.Context, sess session.Store, cpt *captcha.Captcha) string { username := ctx.QueryTrim("username") password := ctx.QueryTrim("password") @@ -78,6 +278,7 @@ func ValidateLogin(ctx *macaron.Context, sess session.Store, cpt *captcha.Captch return json.Success("登录成功", nil) } +// Logout 用户退出 func Logout(ctx *macaron.Context, sess session.Store) { if IsLogin(sess) { err := sess.Destory(ctx) @@ -89,6 +290,7 @@ func Logout(ctx *macaron.Context, sess session.Store) { Login(ctx) } +// Username 获取session中的用户名 func Username(sess session.Store) string { username, ok := sess.Get("username").(string) if ok { @@ -98,6 +300,7 @@ func Username(sess session.Store) string { return "" } +// Uid 获取session中的Uid func Uid(sess session.Store) int { uid, ok := sess.Get("uid").(int) if ok { @@ -107,6 +310,7 @@ func Uid(sess session.Store) int { return 0 } +// IsLogin 判断用户是否已登录 func IsLogin(sess session.Store) bool { uid, ok := sess.Get("uid").(int) if ok && uid > 0 { @@ -116,6 +320,7 @@ func IsLogin(sess session.Store) bool { return false } +// IsAdmin 判断当前用户是否是管理员 func IsAdmin(sess session.Store) bool { isAdmin, ok := sess.Get("isAdmin").(int8) if ok && isAdmin > 0 { diff --git a/templates/common/header.html b/templates/common/header.html index 140bc53..d82f842 100644 --- a/templates/common/header.html +++ b/templates/common/header.html @@ -48,7 +48,7 @@ 你好,{{{.LoginUsername}}} {{{end}}} @@ -62,7 +62,7 @@
保存
- 取消 + 取消 diff --git a/templates/user/editMyPassword.html b/templates/user/editMyPassword.html new file mode 100644 index 0000000..145f485 --- /dev/null +++ b/templates/user/editMyPassword.html @@ -0,0 +1,73 @@ +{{{ template "common/header" . }}} + + + +{{{ template "common/footer" . }}} \ No newline at end of file diff --git a/templates/user/editPassword.html b/templates/user/editPassword.html index a9714aa..762be99 100644 --- a/templates/user/editPassword.html +++ b/templates/user/editPassword.html @@ -3,12 +3,6 @@
修改密码
-
-
- - -
-
@@ -21,7 +15,7 @@
- +
@@ -31,23 +25,14 @@ $('.ui.form').form( { onSuccess: function(event, fields) { - util.post('/user/editPassword', fields, function(code, message) { + util.post('/user/editPassword/{{{.Id}}}', fields, function(code, message) { swal("操作成功", '修改成功', 'success'); - location.href = "/task" + location.href = "/user" }); return false; }, fields: { - oldPassword: { - identifier : 'old_password', - rules: [ - { - type : 'empty', - prompt : '请输入原密码' - } - ] - }, newPassword: { identifier : 'new_password', rules: [ diff --git a/templates/user/index.html b/templates/user/index.html new file mode 100644 index 0000000..acc929c --- /dev/null +++ b/templates/user/index.html @@ -0,0 +1,73 @@ +{{{ template "common/header" . }}} + +
+ {{{ template "user/menu" . }}} + +
+ + + + + + + + + + + + + + {{{range $i, $v := .Users}}} + + + + + + + + + {{{end}}} + +
用户ID用户名邮箱角色状态操作
{{{.Id}}}{{{.Name}}}{{{.Email}}}{{{if .IsAdmin}}}管理员{{{else}}}普通用户{{{end}}}{{{if .Status}}}启用{{{else}}}禁用{{{end}}} + 编辑 + {{{if eq .Status 1}}} +    + {{{else}}} +    + {{{end}}} + + + + +
+ {{{ template "common/pagination" .}}} +
+
+ + + +{{{ template "common/footer" . }}} \ No newline at end of file diff --git a/templates/user/menu.html b/templates/user/menu.html new file mode 100644 index 0000000..09b7d6c --- /dev/null +++ b/templates/user/menu.html @@ -0,0 +1,9 @@ +
+ +
\ No newline at end of file diff --git a/templates/user/user_form.html b/templates/user/user_form.html new file mode 100644 index 0000000..a3f8191 --- /dev/null +++ b/templates/user/user_form.html @@ -0,0 +1,142 @@ +{{{ template "common/header" . }}} + +
+ {{{ template "user/menu" . }}} + +
+ +
+ +
+
+ +
+ +
+
+
+ +
+ +
+
+
+ {{{if eq .User.Id 0}}} +
+
+ + +
+ +
+ + +
+
+ {{{end}}} + +
+
+ + 普通用户 + 管理员 +
+ +
+ + 启用 + 禁用 +
+
+ +
保存
+ 取消 +
+
+
+ + + + +{{{ template "common/footer" . }}} \ No newline at end of file