├── .gitignore ├── README.md ├── app ├── controllers │ ├── agent.go │ ├── base.go │ ├── env.go │ ├── mailtpl.go │ ├── main.go │ ├── project.go │ ├── review.go │ ├── role.go │ ├── server.go │ ├── task.go │ ├── types.go │ └── user.go ├── entity │ ├── action.go │ ├── env.go │ ├── log.go │ ├── mail_tpl.go │ ├── perm.go │ ├── project.go │ ├── role.go │ ├── server.go │ ├── task.go │ └── user.go ├── libs │ ├── cmd.go │ ├── functions.go │ ├── pager.go │ └── ssh.go ├── mail │ └── mail.go └── service │ ├── action.go │ ├── auth.go │ ├── deploy.go │ ├── deploy_job.go │ ├── env.go │ ├── init.go │ ├── mail.go │ ├── project.go │ ├── repository.go │ ├── role.go │ ├── server.go │ ├── system.go │ ├── task.go │ └── user.go ├── assets ├── avatars │ ├── avatar.png │ ├── avatar1.png │ └── avatar2.png ├── css │ ├── ace-ie.min.css │ ├── ace-part2.min.css │ ├── ace-rtl.min.css │ ├── ace.min.css │ ├── bootstrap-datetimepicker.min.css │ ├── bootstrap-duallistbox.min.css │ ├── bootstrap-editable.min.css │ ├── bootstrap-multiselect.min.css │ ├── bootstrap-timepicker.min.css │ ├── bootstrap.min.css │ ├── chosen-sprite.png │ ├── chosen-sprite@2x.png │ ├── chosen.min.css │ ├── colorbox.min.css │ ├── colorpicker.css │ ├── colorpicker.min.css │ ├── datepicker.min.css │ ├── daterangepicker.min.css │ ├── dropzone.min.css │ ├── font-awesome.min.css │ ├── fullcalendar.min.css │ ├── images │ │ ├── border.png │ │ ├── controls.png │ │ ├── loading.gif │ │ ├── loading_background.png │ │ ├── meteorshower2.jpg │ │ ├── overlay.png │ │ └── pattern.jpg │ ├── img │ │ ├── alpha.png │ │ ├── hue.png │ │ └── saturation.png │ ├── jquery-ui.custom.min.css │ ├── jquery-ui.min.css │ ├── jquery.gritter.min.css │ ├── prettify.min.css │ ├── select2-spinner.gif │ ├── select2.min.css │ ├── select2.png │ ├── select2x2.png │ ├── ui.jqgrid.css │ └── ui.jqgrid.min.css ├── font-awesome │ ├── 4.1.0 │ │ ├── css │ │ │ └── font-awesome.min.css │ │ └── fonts │ │ │ ├── fontawesome-webfont.eot │ │ │ ├── fontawesome-webfont.svg │ │ │ ├── fontawesome-webfont.ttf │ │ │ └── fontawesome-webfont.woff │ └── 4.2.0 │ │ ├── css │ │ └── font-awesome.min.css │ │ └── fonts │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.svg │ │ ├── fontawesome-webfont.ttf │ │ └── fontawesome-webfont.woff ├── fonts │ ├── DXI1ORHCpsQm3Vp6mXoaTXhCUOGz7vYGh680lGh-uXM.woff │ ├── cJZKeOuBrn4kERxqtaUH3T8E0i7KZn-EPnyo3HZu7kw.woff │ ├── fonts.googleapis.com.css │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.svg │ ├── glyphicons-halflings-regular.ttf │ └── glyphicons-halflings-regular.woff ├── img │ ├── clear.png │ └── loading.gif └── js │ ├── ace-elements.min.js │ ├── ace-extra.min.js │ ├── ace.min.js │ ├── additional-methods.min.js │ ├── bootbox.min.js │ ├── bootstrap-colorpicker.min.js │ ├── bootstrap-datepicker.min.js │ ├── bootstrap-datetimepicker.min.js │ ├── bootstrap-editable.min.js │ ├── bootstrap-markdown.min.js │ ├── bootstrap-multiselect.min.js │ ├── bootstrap-tag.min.js │ ├── bootstrap-timepicker.min.js │ ├── bootstrap-wysiwyg.min.js │ ├── bootstrap.min.js │ ├── chosen.jquery.min.js │ ├── common.js │ ├── daterangepicker.min.js │ ├── dropzone.min.js │ ├── excanvas.min.js │ ├── fuelux.spinner.min.js │ ├── fuelux.tree.min.js │ ├── fuelux.wizard.min.js │ ├── fullcalendar.min.js │ ├── html5shiv.min.js │ ├── jquery-ui.custom.min.js │ ├── jquery-ui.min.js │ ├── jquery.1.11.1.min.js │ ├── jquery.2.1.1.min.js │ ├── jquery.autosize.min.js │ ├── jquery.bootstrap-duallistbox.min.js │ ├── jquery.colorbox-min.js │ ├── jquery.colorbox.min.js │ ├── jquery.dataTables.bootstrap.js │ ├── jquery.dataTables.bootstrap.min.js │ ├── jquery.dataTables.min.js │ ├── jquery.easypiechart.min.js │ ├── jquery.flot.min.js │ ├── jquery.flot.pie.min.js │ ├── jquery.flot.resize.min.js │ ├── jquery.gritter.min.js │ ├── jquery.hotkeys.min.js │ ├── jquery.inputlimiter.1.3.1.min.js │ ├── jquery.maskedinput.min.js │ ├── jquery.min.js │ ├── jquery.mobile.custom.min.js │ ├── jquery.nestable.min.js │ ├── jquery.raty.min.js │ ├── jquery.slimscroll.min.js │ ├── jquery.sparkline.min.js │ ├── jquery.ui.touch-punch.min.js │ ├── jquery.validate.min.js │ ├── markdown.min.js │ ├── moment.min.js │ ├── prettify.min.js │ ├── respond.min.js │ ├── select2.min.js │ ├── spin.min.js │ └── typeahead.jquery.min.js ├── conf ├── app.conf ├── locale_zh-CN.ini └── menu.json ├── docker-compose.yaml ├── docker ├── Dockerfile ├── app.conf ├── my-custom.cnf └── startup.sh ├── install.sql ├── logs └── .gitkeep ├── main.go ├── pack.sh ├── screenshot.png ├── service.sh └── views ├── agent ├── add.html ├── edit.html ├── list.html └── projects.html ├── env ├── add.html ├── edit.html └── list.html ├── error └── message.html ├── layout ├── layout.html └── sections │ ├── footer.html │ ├── header.html │ ├── navbar.html │ └── sidebar.html ├── log └── list.html ├── mailtpl ├── add.html ├── edit.html └── list.html ├── main ├── index.html ├── login.html └── profile.html ├── project ├── add.html ├── edit.html └── list.html ├── review ├── detail.html ├── list.html └── review.html ├── role ├── add.html ├── edit.html ├── list.html └── perm.html ├── server ├── add.html ├── edit.html ├── list.html └── projects.html ├── task ├── create.html ├── create_step1.html ├── detail.html ├── list.html ├── publish-step1.html ├── publish-step2.html └── publish-step3.html └── user ├── add.html ├── edit.html └── list.html /.gitignore: -------------------------------------------------------------------------------- 1 | gopub* 2 | data/* 3 | logs/*.log 4 | *.tar.gz 5 | *.bak 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 版本发布系统 2 | 3 | 当前版本:v2.0.1 4 | 5 | 基于Git的代码发布系统,用于发布PHP等脚本语言开发的项目。使用Go语言和Beego框架开发。本人所在公司已使用了半年,累计超过五百次发版,到目前为止没出过什么问题,现在功能已经比较完善。 6 | 7 | ## 功能 8 | 9 | 1. 多帐号、多角色、权限管理 10 | 2. 发版邮件通知、邮件模板设置 11 | 3. 支持多个项目,每个项目可设置多个发布环境 12 | 4. 支持发版前、发版后执行指定shell脚本 13 | 5. 支持自动生成版本号文件 14 | 6. 支持发版审批,可针对不同项目选择开启 15 | 16 | ## 流程 17 | 18 | 整个发版流程如下: 19 | 20 | 1. 发布系统构建发布包 21 | 2. 将发布包发布到跳板机 22 | 3. 在跳板机进行解压,将代码同步到目标服务器。 23 | 24 | ## 下载地址 25 | 26 | - [https://github.com/lisijie/gopub/releases](https://github.com/lisijie/gopub/releases) 27 | 28 | ## 安装 29 | 30 | 仅支持linux/mac系统,并且要求安装了mysql和git。 31 | 32 | 安装步骤: 33 | 34 | 1. 创建数据库,将install.sql导入mysql。 35 | 2. 修改 conf/app.conf 中相关的配置。 36 | 3. 使用命令 `./service.sh start` 启动,如果无法启动,检查主程序 gopub 是否具有可执行权限,使用 `chmod +x ./gopub` 增加权限。 37 | 4. 使用 `http://localhost:8000` 访问。 38 | 5. 后台默认帐号为 `admin`,密码为 `admin888`。 39 | 40 | ## 使用docker运行 41 | 42 | 在源码目录使用docker-compose启动即可。 43 | 44 | $ docker-compose up 45 | 46 | ## 界面截图 47 | 48 | ![gopub](https://raw.githubusercontent.com/lisijie/gopub/master/screenshot.png) 49 | -------------------------------------------------------------------------------- /app/controllers/mailtpl.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "github.com/astaxie/beego" 5 | "github.com/lisijie/gopub/app/entity" 6 | "github.com/lisijie/gopub/app/service" 7 | ) 8 | 9 | type MailTplController struct { 10 | BaseController 11 | } 12 | 13 | // 模板列表 14 | func (this *MailTplController) List() { 15 | list, _ := service.MailService.GetMailTplList() 16 | this.Data["pageTitle"] = "邮件模板" 17 | this.Data["list"] = list 18 | this.display() 19 | } 20 | 21 | // 添加模板 22 | func (this *MailTplController) Add() { 23 | if this.isPost() { 24 | name := this.GetString("name") 25 | subject := this.GetString("subject") 26 | content := this.GetString("content") 27 | mailTo := this.GetString("mail_to") 28 | mailCc := this.GetString("mail_cc") 29 | 30 | if name == "" || subject == "" || content == "" { 31 | this.showMsg("模板名称、邮件主题、邮件内容不能为空", MSG_ERR) 32 | } 33 | 34 | tpl := new(entity.MailTpl) 35 | tpl.UserId = this.auth.GetUserId() 36 | tpl.Name = name 37 | tpl.Subject = subject 38 | tpl.Content = content 39 | tpl.MailTo = mailTo 40 | tpl.MailCc = mailCc 41 | err := service.MailService.AddMailTpl(tpl) 42 | this.checkError(err) 43 | 44 | this.redirect(beego.URLFor("MailTplController.List")) 45 | } 46 | 47 | this.Data["pageTitle"] = "添加模板" 48 | this.display() 49 | } 50 | 51 | // 编辑模板 52 | func (this *MailTplController) Edit() { 53 | id, _ := this.GetInt("id") 54 | tpl, err := service.MailService.GetMailTpl(id) 55 | this.checkError(err) 56 | 57 | if this.isPost() { 58 | name := this.GetString("name") 59 | subject := this.GetString("subject") 60 | content := this.GetString("content") 61 | mailTo := this.GetString("mail_to") 62 | mailCc := this.GetString("mail_cc") 63 | if name == "" || subject == "" || content == "" { 64 | this.showMsg("模板名称、邮件主题、邮件内容不能为空", MSG_ERR) 65 | } 66 | 67 | tpl.Name = name 68 | tpl.Subject = subject 69 | tpl.Content = content 70 | tpl.MailTo = mailTo 71 | tpl.MailCc = mailCc 72 | err := service.MailService.SaveMailTpl(tpl) 73 | this.checkError(err) 74 | 75 | this.redirect(beego.URLFor("MailTplController.List")) 76 | } 77 | 78 | this.Data["pageTitle"] = "修改模板" 79 | this.Data["tpl"] = tpl 80 | this.display() 81 | } 82 | 83 | // 删除模板 84 | func (this *MailTplController) Del() { 85 | id, _ := this.GetInt("id") 86 | 87 | err := service.MailService.DelMailTpl(id) 88 | this.checkError(err) 89 | 90 | this.redirect(beego.URLFor("MailTplController.List")) 91 | } 92 | -------------------------------------------------------------------------------- /app/controllers/review.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "github.com/astaxie/beego" 5 | "github.com/lisijie/gopub/app/entity" 6 | "github.com/lisijie/gopub/app/libs" 7 | "github.com/lisijie/gopub/app/service" 8 | ) 9 | 10 | type ReviewController struct { 11 | BaseController 12 | } 13 | 14 | // 列表 15 | func (this *ReviewController) List() { 16 | status, _ := this.GetInt("status") 17 | page, _ := this.GetInt("page") 18 | startDate := this.GetString("start_date") 19 | endDate := this.GetString("end_date") 20 | 21 | if page < 1 { 22 | page = 1 23 | } 24 | filter := make([]interface{}, 0, 6) 25 | if status == 0 { 26 | filter = append(filter, "review_status", 0) 27 | } else { 28 | filter = append(filter, "review_status__in", []int{1, -1}) 29 | } 30 | if startDate != "" { 31 | filter = append(filter, "start_date", startDate) 32 | } 33 | if endDate != "" { 34 | filter = append(filter, "end_date", endDate) 35 | } 36 | 37 | list, count := service.TaskService.GetList(page, this.pageSize, filter...) 38 | envList := make(map[int]*entity.Env) 39 | for k, v := range list { 40 | if _, ok := envList[v.PubEnvId]; !ok { 41 | envList[v.PubEnvId], _ = service.EnvService.GetEnv(v.PubEnvId) 42 | } 43 | list[k].EnvInfo = envList[v.PubEnvId] 44 | } 45 | 46 | this.Data["pageTitle"] = "审批列表" 47 | this.Data["status"] = status 48 | this.Data["count"] = count 49 | this.Data["list"] = list 50 | this.Data["pageBar"] = libs.NewPager(page, int(count), this.pageSize, beego.URLFor("ReviewController.List", "status", status, "start_date", startDate, "end_date", endDate), true).ToString() 51 | this.Data["startDate"] = startDate 52 | this.Data["endDate"] = endDate 53 | this.display() 54 | } 55 | 56 | // 审批 57 | func (this *ReviewController) Review() { 58 | id, _ := this.GetInt("id") 59 | 60 | if this.isPost() { 61 | status, _ := this.GetInt("status") 62 | message := this.GetString("message") 63 | err := service.TaskService.ReviewTask(id, this.userId, status, message) 64 | this.checkError(err) 65 | service.ActionService.Add("review_task", this.auth.GetUserName(), "task", id, this.GetString("status")) 66 | this.redirect(beego.URLFor("ReviewController.List")) 67 | } 68 | 69 | task, err := service.TaskService.GetTask(id) 70 | this.checkError(err) 71 | env, err := service.EnvService.GetEnv(task.PubEnvId) 72 | this.checkError(err) 73 | 74 | this.Data["pageTitle"] = "审批发布单" 75 | this.Data["env"] = env 76 | this.Data["task"] = task 77 | this.display() 78 | } 79 | 80 | // 详情 81 | func (this *ReviewController) Detail() { 82 | id, _ := this.GetInt("id") 83 | 84 | task, err := service.TaskService.GetTask(id) 85 | this.checkError(err) 86 | env, err := service.EnvService.GetEnv(task.PubEnvId) 87 | this.checkError(err) 88 | review, err := service.TaskService.GetReviewInfo(id) 89 | if err != nil { 90 | this.showMsg("审批记录不存在。", MSG_ERR) 91 | } 92 | 93 | this.Data["pageTitle"] = "浏览详情" 94 | this.Data["env"] = env 95 | this.Data["task"] = task 96 | this.Data["review"] = review 97 | this.Data["refer"] = this.Ctx.Request.Referer() 98 | this.display() 99 | } 100 | -------------------------------------------------------------------------------- /app/controllers/role.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "github.com/astaxie/beego" 5 | "github.com/lisijie/gopub/app/entity" 6 | "github.com/lisijie/gopub/app/service" 7 | "strings" 8 | ) 9 | 10 | type RoleController struct { 11 | BaseController 12 | } 13 | 14 | func (this *RoleController) List() { 15 | roleList, err := service.RoleService.GetAllRoles() 16 | this.checkError(err) 17 | for k, role := range roleList { 18 | roleList[k].UserList, _ = service.UserService.GetUserListByRoleId(role.Id) 19 | } 20 | this.Data["pageTitle"] = "角色管理" 21 | this.Data["list"] = roleList 22 | this.display() 23 | } 24 | 25 | func (this *RoleController) Add() { 26 | if this.isPost() { 27 | role := &entity.Role{} 28 | role.RoleName = this.GetString("role_name") 29 | role.Description = this.GetString("description") 30 | if role.RoleName == "" { 31 | this.showMsg("角色名不能为空", MSG_ERR) 32 | } 33 | err := service.RoleService.AddRole(role) 34 | this.checkError(err) 35 | this.redirect(beego.URLFor("RoleController.List")) 36 | } 37 | this.Data["pageTitle"] = "创建角色" 38 | this.display() 39 | } 40 | 41 | func (this *RoleController) Edit() { 42 | id, _ := this.GetInt("id") 43 | role, err := service.RoleService.GetRole(id) 44 | this.checkError(err) 45 | 46 | if this.isPost() { 47 | role.RoleName = this.GetString("role_name") 48 | role.Description = this.GetString("description") 49 | err := service.RoleService.UpdateRole(role, "RoleName", "Description") 50 | this.checkError(err) 51 | this.redirect(beego.URLFor("RoleController.List")) 52 | } 53 | 54 | this.Data["pageTitle"] = "编辑角色" 55 | this.Data["role"] = role 56 | this.display() 57 | } 58 | 59 | func (this *RoleController) Del() { 60 | id, _ := this.GetInt("id") 61 | 62 | err := service.RoleService.DeleteRole(id) 63 | this.checkError(err) 64 | 65 | this.redirect(beego.URLFor("RoleController.List")) 66 | } 67 | 68 | func (this *RoleController) Perm() { 69 | id, _ := this.GetInt("id") 70 | role, err := service.RoleService.GetRole(id) 71 | this.checkError(err) 72 | 73 | if this.isPost() { 74 | pids := this.GetStrings("pids") 75 | perms := this.GetStrings("perms") 76 | if len(pids) == 0 { 77 | role.ProjectIds = "" 78 | } else { 79 | role.ProjectIds = strings.Join(pids, ",") 80 | } 81 | err := service.RoleService.UpdateRole(role, "ProjectIds") 82 | this.checkError(err) 83 | err = service.RoleService.SetPerm(role.Id, perms) 84 | this.checkError(err) 85 | this.redirect(beego.URLFor("RoleController.List")) 86 | } 87 | 88 | projectList, _ := service.ProjectService.GetAllProject() 89 | permList := service.SystemService.GetPermList() 90 | 91 | chkmap := make(map[string]string) 92 | for _, v := range role.PermList { 93 | chkmap[v.Key] = "checked" 94 | } 95 | if role.ProjectIds != "" { 96 | pids := strings.Split(role.ProjectIds, ",") 97 | for _, pid := range pids { 98 | chkmap[pid] = "checked" 99 | } 100 | } 101 | 102 | this.Data["pageTitle"] = "编辑权限" 103 | this.Data["permList"] = permList 104 | this.Data["projectList"] = projectList 105 | this.Data["role"] = role 106 | this.Data["chkmap"] = chkmap 107 | this.display() 108 | } 109 | -------------------------------------------------------------------------------- /app/controllers/server.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "github.com/astaxie/beego" 5 | "github.com/astaxie/beego/validation" 6 | "github.com/lisijie/gopub/app/entity" 7 | "github.com/lisijie/gopub/app/libs" 8 | "github.com/lisijie/gopub/app/service" 9 | "strconv" 10 | ) 11 | 12 | type ServerController struct { 13 | BaseController 14 | } 15 | 16 | // 列表 17 | func (this *ServerController) List() { 18 | page, _ := strconv.Atoi(this.GetString("page")) 19 | if page < 1 { 20 | page = 1 21 | } 22 | count, err := service.ServerService.GetTotal(service.SERVER_TYPE_NORMAL) 23 | this.checkError(err) 24 | serverList, err := service.ServerService.GetServerList(page, this.pageSize) 25 | this.checkError(err) 26 | 27 | this.Data["count"] = count 28 | this.Data["list"] = serverList 29 | this.Data["pageBar"] = libs.NewPager(page, int(count), this.pageSize, beego.URLFor("ServerController.List"), true).ToString() 30 | this.Data["pageTitle"] = "服务器列表" 31 | this.display() 32 | } 33 | 34 | // 添加 35 | func (this *ServerController) Add() { 36 | if this.isPost() { 37 | valid := validation.Validation{} 38 | server := &entity.Server{} 39 | server.TypeId = service.SERVER_TYPE_NORMAL 40 | server.Ip = this.GetString("server_ip") 41 | server.Area = this.GetString("area") 42 | server.Description = this.GetString("description") 43 | valid.Required(server.Ip, "ip").Message("请输入服务器IP") 44 | valid.IP(server.Ip, "ip").Message("服务器IP无效") 45 | if valid.HasErrors() { 46 | for _, err := range valid.Errors { 47 | this.showMsg(err.Message, MSG_ERR) 48 | } 49 | } 50 | 51 | if err := service.ServerService.AddServer(server); err != nil { 52 | this.showMsg(err.Error(), MSG_ERR) 53 | } 54 | 55 | this.redirect(beego.URLFor("ServerController.List")) 56 | } 57 | 58 | this.Data["pageTitle"] = "添加服务器" 59 | this.display() 60 | } 61 | 62 | // 编辑 63 | func (this *ServerController) Edit() { 64 | id, _ := this.GetInt("id") 65 | server, err := service.ServerService.GetServer(id, service.SERVER_TYPE_NORMAL) 66 | this.checkError(err) 67 | 68 | if this.isPost() { 69 | valid := validation.Validation{} 70 | ip := this.GetString("server_ip") 71 | server.Area = this.GetString("area") 72 | server.Description = this.GetString("description") 73 | valid.Required(ip, "ip").Message("请输入服务器IP") 74 | valid.IP(ip, "ip").Message("服务器IP无效") 75 | if valid.HasErrors() { 76 | for _, err := range valid.Errors { 77 | this.showMsg(err.Message, MSG_ERR) 78 | } 79 | } 80 | server.Ip = ip 81 | err := service.ServerService.UpdateServer(server) 82 | this.checkError(err) 83 | this.redirect(beego.URLFor("ServerController.List")) 84 | } 85 | 86 | this.Data["pageTitle"] = "编辑服务器" 87 | this.Data["server"] = server 88 | this.display() 89 | } 90 | 91 | // 删除 92 | func (this *ServerController) Del() { 93 | id, _ := this.GetInt("id") 94 | 95 | _, err := service.ServerService.GetServer(id, service.SERVER_TYPE_NORMAL) 96 | this.checkError(err) 97 | 98 | err = service.ServerService.DeleteServer(id) 99 | this.checkError(err) 100 | 101 | this.redirect(beego.URLFor("ServerController.List")) 102 | } 103 | 104 | // 项目列表 105 | func (this *ServerController) Projects() { 106 | id, _ := this.GetInt("id") 107 | server, err := service.ServerService.GetServer(id, service.SERVER_TYPE_NORMAL) 108 | this.checkError(err) 109 | envList, err := service.EnvService.GetEnvListByServerId(id) 110 | this.checkError(err) 111 | 112 | result := make(map[int]map[string]interface{}) 113 | for _, env := range envList { 114 | if _, ok := result[env.ProjectId]; !ok { 115 | project, err := service.ProjectService.GetProject(env.ProjectId) 116 | if err != nil { 117 | continue 118 | } 119 | row := make(map[string]interface{}) 120 | row["projectId"] = project.Id 121 | row["projectName"] = project.Name 122 | row["envName"] = env.Name 123 | result[env.ProjectId] = row 124 | } else { 125 | result[env.ProjectId]["envName"] = result[env.ProjectId]["envName"].(string) + ", " + env.Name 126 | } 127 | } 128 | 129 | this.Data["list"] = result 130 | this.Data["server"] = server 131 | this.Data["pageTitle"] = server.Ip + " 下的项目列表" 132 | this.display() 133 | } 134 | -------------------------------------------------------------------------------- /app/controllers/types.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | type Menu struct { 4 | Name string 5 | Route string 6 | Icon string 7 | Submenu []SubMenu 8 | } 9 | 10 | type SubMenu struct { 11 | Name string 12 | Route string 13 | Action string 14 | } 15 | -------------------------------------------------------------------------------- /app/entity/action.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // 用户动作 8 | type Action struct { 9 | Id int 10 | Action string `orm:"size(20)"` // 动作类型 11 | Actor string `orm:"size(20)"` // 操作角色 12 | ObjectType string `orm:"size(20)"` // 操作对象类型 13 | ObjectId int `orm:"default(0)"` // 操作对象id 14 | Extra string `orm:"size(1000)"` // 额外信息 15 | CreateTime time.Time `orm:"auto_now;type(datetime)"` // 更新时间 16 | Message string `orm:"-"` // 格式化后的消息 17 | } 18 | -------------------------------------------------------------------------------- /app/entity/env.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // 发布环境 8 | type Env struct { 9 | Id int 10 | ProjectId int `orm:"index"` // 项目id 11 | Name string `orm:"size(20)"` // 发布环境名称 12 | SshUser string `orm:"size(20)"` // 发布帐号 13 | SshPort string `orm:"size(10)"` // SSH端口 14 | SshKey string `orm:"size(100)"` // SSH KEY路径 15 | PubDir string `orm:"size(100)"` // 发布目录 16 | BeforeShell string `orm:"type(text)"` // 发布前执行的shell脚本 17 | AfterShell string `orm:"type(text)"` // 发布后执行的shell脚本 18 | ServerCount int `orm:"default(0)"` // 服务器数量 19 | SendMail int `orm:"default(0)"` // 是否发送发版邮件通知 20 | MailTplId int `orm:"default(0)"` // 邮件模板id 21 | MailTo string `orm:"size(1000)"` // 邮件收件人 22 | MailCc string `orm:"size(1000)"` // 邮件抄送人 23 | CreateTime time.Time `orm:"auto_now_add;type(datetime)"` // 创建时间 24 | UpdateTime time.Time `orm:"auto_now;type(datetime)"` // 更新时间 25 | ServerList []Server `orm:"-"` // 服务器列表 26 | } 27 | 28 | // 表结构 29 | type EnvServer struct { 30 | Id int 31 | ProjectId int `orm:"default(0)"` // 项目id 32 | EnvId int `orm:"default(0);index"` // 环境id 33 | ServerId int `orm:"default(0)"` // 服务器id 34 | } 35 | -------------------------------------------------------------------------------- /app/entity/log.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // 表结构 8 | type Log struct { 9 | Id int 10 | UserId int 11 | Action string `orm:"size(100)"` // 操作 12 | Url string `orm:"size(200)"` // url 13 | PostData string `orm:"type(text)"` // 提交数据 14 | Message string `orm:"type(text)"` // 消息内容 15 | CreateTime time.Time `orm:"auto_now_add;type(datetime)"` // 创建时间 16 | } 17 | -------------------------------------------------------------------------------- /app/entity/mail_tpl.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // 表结构 8 | type MailTpl struct { 9 | Id int 10 | UserId int 11 | Name string `orm:"size(100)"` // 模板名 12 | Subject string `orm:"size(200)"` // 邮件主题 13 | Content string `orm:"type(text)"` // 邮件内容 14 | MailTo string `orm:"size(1000)"` // 预设收件人 15 | MailCc string `orm:"size(1000)"` // 预设抄送人 16 | CreateTime time.Time `orm:"auto_now_add;type(datetime)"` // 创建时间 17 | UpdateTime time.Time `orm:"auto_now;type(datetime)"` // 更新时间 18 | } 19 | -------------------------------------------------------------------------------- /app/entity/perm.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | type Perm struct { 4 | Module string `orm:"size(20)"` 5 | Action string `orm:"size(20)"` 6 | Key string `orm:"-"` // Module.Action 7 | } 8 | 9 | func (p *Perm) TableUnique() [][]string { 10 | return [][]string{ 11 | []string{"Module", "Action"}, 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/entity/project.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // 表结构 8 | type Project struct { 9 | Id int 10 | Name string `orm:"size(100)"` // 项目名称 11 | Domain string `orm:"size(100)"` // 项目标识 12 | Version string `orm:"size(20)"` // 最后发布版本 13 | VersionTime time.Time `orm:"type(datetime)"` // 最后发版时间 14 | RepoUrl string `orm:"size(100)"` // 仓库地址 15 | Status int `orm:"default(0)"` // 初始化状态 16 | ErrorMsg string `orm:"type(text)"` // 错误消息 17 | AgentId int `orm:"default(0)"` // 跳板机ID 18 | IgnoreList string `orm:"type(text)"` // 忽略文件列表 19 | BeforeShell string `orm:"type(text)"` // 发布前要执行的shell脚本 20 | AfterShell string `orm:"type(text)"` // 发布后要执行的shell脚本 21 | CreateVerfile int `orm:"default(0)"` // 是否生成版本号文件 22 | VerfilePath string `orm:"size(50)"` // 版本号文件目录 23 | TaskReview int `orm:"default(0)"` // 发布单是否需要经过审批 24 | CreateTime time.Time `orm:"auto_now_add;type(datetime)"` // 创建时间 25 | UpdateTime time.Time `orm:"auto_now;type(datetime)"` // 更新时间 26 | } 27 | -------------------------------------------------------------------------------- /app/entity/role.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // 角色 8 | type Role struct { 9 | Id int 10 | RoleName string `orm:"size(20)"` // 角色名称 11 | ProjectIds string `orm:"size(1000)"` // 项目权限 12 | Description string `orm:"size(200)"` // 说明 13 | CreateTime time.Time `orm:"auto_now_add;type(datetime)"` // 创建时间 14 | UpdateTime time.Time `orm:"auto_now;type(datetime)"` // 更新时间 15 | PermList []Perm `orm:"-"` // 权限列表 16 | UserList []User `orm:"-"` // 用户列表 17 | } 18 | 19 | // 角色权限 20 | type RolePerm struct { 21 | RoleId int // 角色id 22 | Perm string `orm:"size(50)"` // 权限 23 | } 24 | -------------------------------------------------------------------------------- /app/entity/server.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // 表结构 8 | type Server struct { 9 | Id int 10 | TypeId int // 0:普通服务器, 1:跳板机 11 | Ip string `orm:"size(20)"` // 服务器IP 12 | Area string `orm:"size(20)"` // 机房 13 | Description string `orm:"size(200)"` // 服务器说明 14 | SshPort int // ssh端口 15 | SshUser string `orm:"size(50)"` // ssh用户 16 | SshPwd string `orm:"size(100)"` // ssh密码 17 | SshKey string `orm:"size(100)"` // ssh key路径 18 | WorkDir string `orm:"size(100)"` // 工作目录 19 | CreateTime time.Time `orm:"auto_now_add;type(datetime)"` // 创建时间 20 | UpdateTime time.Time `orm:"auto_now;type(datetime)"` // 更新时间 21 | } 22 | -------------------------------------------------------------------------------- /app/entity/task.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | type Task struct { 11 | Id int 12 | ProjectId int `orm:"index"` // 项目id 13 | StartVer string `orm:"size(20)"` // 起始版本号 14 | EndVer string `orm:"size(20)"` // 结束版本号 15 | Message string `orm:"type(text)"` // 版本说明 16 | UserId int `orm:"index"` // 创建人ID 17 | UserName string `orm:"size(20)"` // 创建人名称 18 | BuildStatus int `orm:"default(0)"` // 构建状态 19 | ChangeLogs string `orm:"type(text)"` // 修改日志列表 20 | ChangeFiles string `orm:"type(text)"` // 修改文件列表 21 | Filepath string `orm:"size(200)"` // 更新包路径 22 | PubEnvId int `orm:"default(0)"` // 发布环境ID 23 | PubStatus int `orm:"default(0)"` // 发布状态:1 正在发布,2 发布到跳板机,3 发布到目标服务器,-2 发布到跳板机失败,-3 发布到目标服务器失败 24 | PubTime time.Time `orm:"null;type(datetime)"` // 发布时间 25 | ErrorMsg string `orm:"type(text)"` // 错误消息 26 | PubLog string `orm:"type(text)"` // 发布日志 27 | ReviewStatus int `orm:"default(0)"` // 审批状态 28 | CreateTime time.Time `orm:"auto_now_add;type(datetime)"` // 创建时间 29 | UpdateTime time.Time `orm:"auto_now;type(datetime)"` // 更新时间 30 | ProjectInfo *Project `orm:"-"` // 项目信息 31 | EnvInfo *Env `orm:"-"` // 发布环境 32 | } 33 | 34 | func (t *Task) GetChangeFileStat() string { 35 | var modifyNum, addNum, deleteNum int 36 | scaner := bufio.NewScanner(strings.NewReader(t.ChangeFiles)) 37 | for scaner.Scan() { 38 | line := scaner.Bytes() 39 | switch line[0] { 40 | case 'M': 41 | modifyNum++ 42 | case 'A': 43 | addNum++ 44 | case 'D': 45 | deleteNum++ 46 | } 47 | } 48 | return fmt.Sprintf("总数:%d,新增:%d,修改:%d,删除:%d", modifyNum+addNum+deleteNum, addNum, modifyNum, deleteNum) 49 | } 50 | 51 | type TaskReview struct { 52 | Id int 53 | TaskId int `orm:"default(0)"` // 任务id 54 | UserId int `orm:"default(0)"` // 审批人id 55 | UserName string `orm:"size(20)"` // 审批人 56 | Status int `orm:"default(0)"` // 审批结果(1:通过;0:不通过) 57 | Message string `orm:"type(text)"` // 审批说明 58 | CreateTime time.Time `orm:"auto_now_add;type(datetime)"` // 创建时间 59 | } 60 | -------------------------------------------------------------------------------- /app/entity/user.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type User struct { 8 | Id int 9 | UserName string `orm:"unique;size(20)"` // 用户名 10 | Password string `orm:"size(32)"` // 密码 11 | Salt string `orm:"size(10)"` // 密码盐 12 | Sex int `orm:"default(0)"` // 性别 13 | Email string `orm:"size(50)"` // 邮箱 14 | LastLogin time.Time `orm:"null;type(datetime)"` // 最后登录时间 15 | LastIp string `orm:"size(15)"` // 最后登录IP 16 | Status int `orm:"default(0)"` // 状态,0正常 -1禁用 17 | CreateTime time.Time `orm:"auto_now_add;type(datetime)"` // 创建时间 18 | UpdateTime time.Time `orm:"auto_now;type(datetime)"` // 更新时间 19 | RoleList []Role `orm:"-"` // 角色列表 20 | } 21 | 22 | type UserRole struct { 23 | UserId int // 用户id 24 | RoleId int // 角色id 25 | } 26 | -------------------------------------------------------------------------------- /app/libs/functions.go: -------------------------------------------------------------------------------- 1 | package libs 2 | 3 | import ( 4 | "crypto/md5" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "regexp" 9 | "strconv" 10 | "strings" 11 | ) 12 | 13 | var emailPattern = regexp.MustCompile("[\\w!#$%&'*+/=?^_`{|}~-]+(?:\\.[\\w!#$%&'*+/=?^_`{|}~-]+)*@(?:[\\w](?:[\\w-]*[\\w])?\\.)+[a-zA-Z0-9](?:[\\w-]*[\\w])?") 14 | 15 | const prettyLogFormat = `--pretty=format:%H` 16 | 17 | func RefEndName(refStr string) string { 18 | if strings.HasPrefix(refStr, "refs/heads/") { 19 | // trim the "refs/heads/" 20 | return refStr[len("refs/heads/"):] 21 | } 22 | 23 | index := strings.LastIndex(refStr, "/") 24 | if index != -1 { 25 | return refStr[index+1:] 26 | } 27 | return refStr 28 | } 29 | 30 | func filepathFromSHA1(rootdir, sha1 string) string { 31 | return filepath.Join(rootdir, "objects", sha1[:2], sha1[2:]) 32 | } 33 | 34 | func IsDir(dir string) bool { 35 | f, e := os.Stat(dir) 36 | if e != nil { 37 | return false 38 | } 39 | return f.IsDir() 40 | } 41 | 42 | func IsFile(filePath string) bool { 43 | f, e := os.Stat(filePath) 44 | if e != nil { 45 | return false 46 | } 47 | return !f.IsDir() 48 | } 49 | 50 | func RealPath(filePath string) string { 51 | return os.ExpandEnv(filePath) 52 | } 53 | 54 | // 版本对比 v1比v2大返回1,小于返回-1,等于返回0 55 | func VerCompare(ver1, ver2 string) int { 56 | ver1 = strings.TrimLeft(ver1, "ver") // 清除v,e,r 57 | ver2 = strings.TrimLeft(ver2, "ver") // 清除v,e,r 58 | p1 := strings.Split(ver1, ".") 59 | p2 := strings.Split(ver2, ".") 60 | 61 | ver1 = "" 62 | for _, v := range p1 { 63 | iv, _ := strconv.Atoi(v) 64 | ver1 = fmt.Sprintf("%s%04d", ver1, iv) 65 | } 66 | 67 | ver2 = "" 68 | for _, v := range p2 { 69 | iv, _ := strconv.Atoi(v) 70 | ver2 = fmt.Sprintf("%s%04d", ver2, iv) 71 | } 72 | 73 | return strings.Compare(ver1, ver2) 74 | } 75 | 76 | // 生成md5 77 | func Md5(buf []byte) string { 78 | hash := md5.New() 79 | hash.Write(buf) 80 | return fmt.Sprintf("%x", hash.Sum(nil)) 81 | } 82 | 83 | // 换行符换成
84 | func Nl2br(s string) string { 85 | s = strings.Replace(s, "\r\n", "\n", -1) 86 | s = strings.Replace(s, "\r", "\n", -1) 87 | s = strings.Replace(s, "\n", "
", -1) 88 | return s 89 | } 90 | 91 | func IsEmail(b []byte) bool { 92 | return emailPattern.Match(b) 93 | } 94 | -------------------------------------------------------------------------------- /app/libs/pager.go: -------------------------------------------------------------------------------- 1 | package libs 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "math" 7 | "strings" 8 | ) 9 | 10 | type Pager struct { 11 | Page int 12 | Totalnum int 13 | Pagesize int 14 | urlpath string 15 | urlquery string 16 | nopath bool 17 | } 18 | 19 | func NewPager(page, totalnum, pagesize int, url string, nopath ...bool) *Pager { 20 | p := new(Pager) 21 | p.Page = page 22 | p.Totalnum = totalnum 23 | p.Pagesize = pagesize 24 | 25 | arr := strings.Split(url, "?") 26 | p.urlpath = arr[0] 27 | if len(arr) > 1 { 28 | p.urlquery = "?" + arr[1] 29 | } else { 30 | p.urlquery = "" 31 | } 32 | 33 | if len(nopath) > 0 { 34 | p.nopath = nopath[0] 35 | } else { 36 | p.nopath = false 37 | } 38 | 39 | return p 40 | } 41 | 42 | func (this *Pager) url(page int) string { 43 | if this.nopath { //不使用目录形式 44 | if this.urlquery != "" { 45 | return fmt.Sprintf("%s%s&page=%d", this.urlpath, this.urlquery, page) 46 | } else { 47 | return fmt.Sprintf("%s?page=%d", this.urlpath, page) 48 | } 49 | } else { 50 | return fmt.Sprintf("%s/page/%d%s", this.urlpath, page, this.urlquery) 51 | } 52 | } 53 | 54 | func (this *Pager) ToString() string { 55 | if this.Totalnum <= this.Pagesize { 56 | return "" 57 | } 58 | 59 | var buf bytes.Buffer 60 | var from, to, linknum, offset, totalpage int 61 | 62 | offset = 5 63 | linknum = 10 64 | 65 | totalpage = int(math.Ceil(float64(this.Totalnum) / float64(this.Pagesize))) 66 | 67 | if totalpage < linknum { 68 | from = 1 69 | to = totalpage 70 | } else { 71 | from = this.Page - offset 72 | to = from + linknum 73 | if from < 1 { 74 | from = 1 75 | to = from + linknum - 1 76 | } else if to > totalpage { 77 | to = totalpage 78 | from = totalpage - linknum + 1 79 | } 80 | } 81 | 82 | buf.WriteString("") 111 | 112 | return buf.String() 113 | } 114 | -------------------------------------------------------------------------------- /app/libs/ssh.go: -------------------------------------------------------------------------------- 1 | package libs 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/pkg/sftp" 7 | "golang.org/x/crypto/ssh" 8 | "io" 9 | "io/ioutil" 10 | "os" 11 | "path" 12 | ) 13 | 14 | type ServerConn struct { 15 | addr string // 192.168.1.1:22 16 | user string 17 | key string 18 | conn *ssh.Client 19 | sftpClient *sftp.Client 20 | } 21 | 22 | func NewServerConn(addr, user, key string) *ServerConn { 23 | key = RealPath(key) 24 | return &ServerConn{ 25 | addr: addr, 26 | user: user, 27 | key: key, 28 | } 29 | } 30 | 31 | // 连接ssh服务器 32 | func (s *ServerConn) getSshConnect() (*ssh.Client, error) { 33 | if s.conn != nil { 34 | return s.conn, nil 35 | } 36 | config := ssh.ClientConfig{ 37 | User: s.user, 38 | } 39 | 40 | keys := []ssh.Signer{} 41 | if pk, err := readPrivateKey(s.key); err == nil { 42 | keys = append(keys, pk) 43 | } 44 | config.Auth = append(config.Auth, ssh.PublicKeys(keys...)) 45 | 46 | conn, err := ssh.Dial("tcp", s.addr, &config) 47 | if err != nil { 48 | return nil, fmt.Errorf("无法连接到服务器 [%s]: %v", s.addr, err) 49 | } 50 | s.conn = conn 51 | return s.conn, nil 52 | } 53 | 54 | // 返回sftp连接 55 | func (s *ServerConn) getSftpConnect() (*sftp.Client, error) { 56 | if s.sftpClient != nil { 57 | return s.sftpClient, nil 58 | } 59 | 60 | conn, err := s.getSshConnect() 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | s.sftpClient, err = sftp.NewClient(conn, sftp.MaxPacket(1<<15)) 66 | return s.sftpClient, err 67 | } 68 | 69 | // 关闭连接 70 | func (s *ServerConn) Close() { 71 | if s.conn != nil { 72 | s.conn.Close() 73 | s.conn = nil 74 | } 75 | if s.sftpClient != nil { 76 | s.sftpClient.Close() 77 | s.sftpClient = nil 78 | } 79 | } 80 | 81 | // 尝试连接服务器 82 | func (s *ServerConn) TryConnect() error { 83 | _, err := s.getSshConnect() 84 | if err != nil { 85 | return err 86 | } 87 | s.Close() 88 | return nil 89 | } 90 | 91 | // 在远程服务器执行命令 92 | func (s *ServerConn) RunCmd(cmd string) (string, error) { 93 | 94 | conn, err := s.getSshConnect() 95 | if err != nil { 96 | return "", err 97 | } 98 | 99 | session, err := conn.NewSession() 100 | if err != nil { 101 | return "", fmt.Errorf("创建会话失败: %v", err) 102 | } 103 | defer session.Close() 104 | 105 | var buf bytes.Buffer 106 | 107 | session.Stdout = &buf 108 | session.Stdin = &buf 109 | 110 | if err := session.Run(cmd); err != nil { 111 | return "", fmt.Errorf("执行命令失败: %v", err) 112 | } 113 | 114 | return buf.String(), nil 115 | 116 | } 117 | 118 | // 拷贝本机文件到远程服务器 119 | func (s *ServerConn) CopyFile(srcFile, dstFile string) error { 120 | client, err := s.getSftpConnect() 121 | if err != nil { 122 | return err 123 | } 124 | 125 | toPath := path.Dir(dstFile) 126 | if _, err := s.RunCmd("mkdir -p " + toPath); err != nil { 127 | return fmt.Errorf("创建目录失败:%v", err) 128 | } 129 | 130 | f, err := os.Open(srcFile) 131 | if err != nil { 132 | return fmt.Errorf("打开本地文件失败: %v", err) 133 | } 134 | defer f.Close() 135 | 136 | w, err := client.Create(dstFile) 137 | if err != nil { 138 | return fmt.Errorf("创建文件失败 [%s]: %v", dstFile, err) 139 | } 140 | defer w.Close() 141 | 142 | n, err := io.Copy(w, f) 143 | if err != nil { 144 | return fmt.Errorf("拷贝文件失败: %v", err) 145 | } 146 | 147 | fstat, _ := f.Stat() 148 | if fstat.Size() != n { 149 | return fmt.Errorf("写入文件大小错误,源文件大小:%d, 写入大小:%d", fstat.Size(), n) 150 | } 151 | 152 | return nil 153 | } 154 | 155 | func readPrivateKey(path string) (ssh.Signer, error) { 156 | f, err := os.Open(path) 157 | if err != nil { 158 | return nil, err 159 | } 160 | defer f.Close() 161 | b, err := ioutil.ReadAll(f) 162 | if err != nil { 163 | return nil, err 164 | } 165 | return ssh.ParsePrivateKey(b) 166 | } 167 | -------------------------------------------------------------------------------- /app/mail/mail.go: -------------------------------------------------------------------------------- 1 | package mail 2 | 3 | import ( 4 | "github.com/astaxie/beego" 5 | "github.com/lisijie/gomail" 6 | "github.com/lisijie/gopub/app/libs" 7 | "strings" 8 | ) 9 | 10 | var ( 11 | host string 12 | port int 13 | username string 14 | password string 15 | from string 16 | ) 17 | 18 | func init() { 19 | host = beego.AppConfig.String("mail.host") 20 | port, _ = beego.AppConfig.Int("mail.port") 21 | username = beego.AppConfig.String("mail.user") 22 | password = beego.AppConfig.String("mail.password") 23 | from = beego.AppConfig.String("mail.from") 24 | if port == 0 { 25 | port = 25 26 | } 27 | } 28 | 29 | func SendMail(subject, content string, to, cc []string) error { 30 | toList := make([]string, 0, len(to)) 31 | ccList := make([]string, 0, len(cc)) 32 | 33 | for _, v := range to { 34 | v = strings.TrimSpace(v) 35 | if libs.IsEmail([]byte(v)) { 36 | exists := false 37 | for _, vv := range toList { 38 | if v == vv { 39 | exists = true 40 | break 41 | } 42 | } 43 | if !exists { 44 | toList = append(toList, v) 45 | } 46 | } 47 | } 48 | for _, v := range cc { 49 | v = strings.TrimSpace(v) 50 | if libs.IsEmail([]byte(v)) { 51 | exists := false 52 | for _, vv := range ccList { 53 | if v == vv { 54 | exists = true 55 | break 56 | } 57 | } 58 | if !exists { 59 | ccList = append(ccList, v) 60 | } 61 | } 62 | } 63 | 64 | m := gomail.NewMessage() 65 | m.SetHeader("From", from) 66 | m.SetHeader("To", toList...) 67 | if len(ccList) > 0 { 68 | m.SetHeader("Cc", ccList...) 69 | } 70 | m.SetHeader("Subject", subject) 71 | m.SetBody("text/html", content) 72 | 73 | d := gomail.NewPlainDialer(host, port, username, password) 74 | 75 | return d.DialAndSend(m) 76 | } 77 | -------------------------------------------------------------------------------- /app/service/action.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "fmt" 5 | "github.com/lisijie/gopub/app/entity" 6 | ) 7 | 8 | // 系统动态 9 | type actionService struct{} 10 | 11 | func (this *actionService) table() string { 12 | return tableName("action") 13 | } 14 | 15 | // 添加记录 16 | func (this *actionService) Add(action, actor, objectType string, objectId int, extra string) bool { 17 | act := new(entity.Action) 18 | act.Action = action 19 | act.Actor = actor 20 | act.ObjectType = objectType 21 | act.ObjectId = objectId 22 | act.Extra = extra 23 | o.Insert(act) 24 | return true 25 | } 26 | 27 | // 登录动态 28 | func (this *actionService) Login(userName string, userId int, ip string) { 29 | this.Add("login", userName, "user", userId, ip) 30 | } 31 | 32 | // 退出登录 33 | func (this *actionService) Logout(userName string, userId int, ip string) { 34 | this.Add("logout", userName, "user", userId, ip) 35 | } 36 | 37 | // 更新个人信息 38 | func (this *actionService) UpdateProfile(userName string, userId int) { 39 | this.Add("update_profile", userName, "user", userId, "") 40 | } 41 | 42 | // 获取动态列表 43 | func (this *actionService) GetList(page, pageSize int) ([]entity.Action, error) { 44 | var list []entity.Action 45 | num, err := o.QueryTable(this.table()).OrderBy("-create_time").Offset((page - 1) * pageSize).Limit(pageSize).All(&list) 46 | if num > 0 && err == nil { 47 | for i := 0; i < int(num); i++ { 48 | this.format(&list[i]) 49 | } 50 | } 51 | return list, err 52 | } 53 | 54 | // 格式化 55 | func (this *actionService) format(action *entity.Action) { 56 | switch action.Action { 57 | case "login": 58 | action.Message = fmt.Sprintf("%s 登录系统,IP为 %s。", action.Actor, action.Extra) 59 | case "logout": 60 | action.Message = fmt.Sprintf("%s 退出系统。", action.Actor) 61 | case "update_profile": 62 | action.Message = fmt.Sprintf("%s 更新了个人资料。", action.Actor) 63 | case "create_task": 64 | action.Message = fmt.Sprintf("%s 创建了编号为 %d 的发布单。", action.Actor, action.ObjectId) 65 | case "pub_task": 66 | task, err := TaskService.GetTask(action.ObjectId) 67 | if err != nil { 68 | action.Message = fmt.Sprintf("%s 发布了编号为 %d 版本。", action.Actor, action.ObjectId) 69 | } else { 70 | action.Message = fmt.Sprintf("%s 发布了 %s%s 版本。", action.Actor, task.ProjectInfo.Name, task.EndVer) 71 | } 72 | case "del_task": 73 | action.Message = fmt.Sprintf("%s 删除了编号为 %d 的发布单。", action.Actor, action.ObjectId) 74 | case "review_task": 75 | task, err := TaskService.GetTask(action.ObjectId) 76 | if err != nil { 77 | if action.Extra == "1" { 78 | action.Message = fmt.Sprintf("%s 审批了编号为 %d 的发布单,结果为:通过", action.Actor, action.ObjectId) 79 | } else { 80 | action.Message = fmt.Sprintf("%s 审批了编号为 %d 的发布单,结果为:不通过", action.Actor, action.ObjectId) 81 | } 82 | } else { 83 | if action.Extra == "1" { 84 | action.Message = fmt.Sprintf("%s 审批了 %s 编号为%d的发布单,结果为:通过", action.Actor, task.ProjectInfo.Name, action.ObjectId) 85 | } else if action.Extra == "-1" { 86 | action.Message = fmt.Sprintf("%s 审批了 %s 编号为%d的发布单,结果为:不通过", action.Actor, task.ProjectInfo.Name, action.ObjectId) 87 | } 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /app/service/auth.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/astaxie/beego" 7 | "github.com/astaxie/beego/orm" 8 | "github.com/lisijie/gopub/app/entity" 9 | "github.com/lisijie/gopub/app/libs" 10 | "strconv" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | // 登录验证服务 16 | type AuthService struct { 17 | loginUser *entity.User // 当前登录用户 18 | permMap map[string]bool // 当前用户权限表 19 | openPerm map[string]bool // 公开的权限 20 | } 21 | 22 | func NewAuth() *AuthService { 23 | return new(AuthService) 24 | } 25 | 26 | // 初始化开放权限 27 | func (this *AuthService) initOpenPerm() { 28 | this.openPerm = map[string]bool{ 29 | "main.index": true, 30 | "main.profile": true, 31 | "main.login": true, 32 | "main.logout": true, 33 | "main.getpubstat": true, 34 | "project.clone": true, 35 | "project.getstatus": true, 36 | "task.gettags": true, 37 | "task.getstatus": true, 38 | "task.startpub": true, 39 | } 40 | } 41 | 42 | // 获取当前登录的用户对象 43 | func (this *AuthService) GetUser() *entity.User { 44 | return this.loginUser 45 | } 46 | 47 | // 获取当前登录的用户id 48 | func (this *AuthService) GetUserId() int { 49 | if this.IsLogined() { 50 | return this.loginUser.Id 51 | } 52 | return 0 53 | } 54 | 55 | // 获取当前登录的用户名 56 | func (this *AuthService) GetUserName() string { 57 | if this.IsLogined() { 58 | return this.loginUser.UserName 59 | } 60 | return "" 61 | } 62 | 63 | // 初始化 64 | func (this *AuthService) Init(token string) { 65 | this.initOpenPerm() 66 | arr := strings.Split(token, "|") 67 | beego.Trace("登录验证, token: ", token) 68 | if len(arr) == 2 { 69 | idstr, password := arr[0], arr[1] 70 | userId, _ := strconv.Atoi(idstr) 71 | if userId > 0 { 72 | user, err := UserService.GetUser(userId, true) 73 | if err == nil && password == libs.Md5([]byte(user.Password+user.Salt)) { 74 | this.loginUser = user 75 | this.initPermMap() 76 | beego.Trace("验证成功,用户信息: ", user) 77 | } 78 | } 79 | } 80 | } 81 | 82 | // 初始化权限表 83 | func (this *AuthService) initPermMap() { 84 | this.permMap = make(map[string]bool) 85 | for _, role := range this.loginUser.RoleList { 86 | for _, perm := range role.PermList { 87 | this.permMap[perm.Key] = true 88 | } 89 | } 90 | } 91 | 92 | // 检查是否有某个权限 93 | func (this *AuthService) HasAccessPerm(module, action string) bool { 94 | key := module + "." + action 95 | if !this.IsLogined() { 96 | return false 97 | } 98 | if this.loginUser.Id == 1 || this.isOpenPerm(key) { 99 | return true 100 | } 101 | if _, ok := this.permMap[key]; ok { 102 | return true 103 | } 104 | return false 105 | } 106 | 107 | // 检查是否登录 108 | func (this *AuthService) IsLogined() bool { 109 | return this.loginUser != nil && this.loginUser.Id > 0 110 | } 111 | 112 | // 是否公开访问的操作 113 | func (this *AuthService) isOpenPerm(key string) bool { 114 | if _, ok := this.openPerm[key]; ok { 115 | return true 116 | } 117 | return false 118 | } 119 | 120 | // 用户登录 121 | func (this *AuthService) Login(userName, password string) (string, error) { 122 | user, err := UserService.GetUserByName(userName) 123 | if err != nil { 124 | if err == orm.ErrNoRows { 125 | return "", errors.New("帐号或密码错误") 126 | } else { 127 | return "", errors.New("系统错误") 128 | } 129 | } 130 | 131 | if user.Password != libs.Md5([]byte(password+user.Salt)) { 132 | return "", errors.New("帐号或密码错误") 133 | } 134 | if user.Status == -1 { 135 | return "", errors.New("该帐号已禁用") 136 | } 137 | 138 | user.LastLogin = time.Now() 139 | UserService.UpdateUser(user, "LastLogin") 140 | this.loginUser = user 141 | 142 | token := fmt.Sprintf("%d|%s", user.Id, libs.Md5([]byte(user.Password+user.Salt))) 143 | return token, nil 144 | } 145 | 146 | // 退出登录 147 | func (this *AuthService) Logout() error { 148 | return nil 149 | } 150 | -------------------------------------------------------------------------------- /app/service/env.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/astaxie/beego/orm" 5 | "github.com/lisijie/gopub/app/entity" 6 | ) 7 | 8 | type envService struct{} 9 | 10 | func (this *envService) table() string { 11 | return tableName("env") 12 | } 13 | func (this *envService) serverTable() string { 14 | return tableName("env_server") 15 | } 16 | 17 | // 获取一个发布环境信息 18 | func (this *envService) GetEnv(id int) (*entity.Env, error) { 19 | env := &entity.Env{} 20 | env.Id = id 21 | err := o.Read(env) 22 | if err == nil { 23 | env.ServerList, _ = this.GetEnvServers(env.Id) 24 | } 25 | return env, err 26 | } 27 | 28 | // 获取某个项目的发布环境列表 29 | func (this *envService) GetEnvListByProjectId(projectId int) ([]entity.Env, error) { 30 | var list []entity.Env 31 | _, err := o.QueryTable(this.table()).Filter("project_id", projectId).All(&list) 32 | for _, env := range list { 33 | env.ServerList, _ = this.GetEnvServers(env.Id) 34 | } 35 | return list, err 36 | } 37 | 38 | // 根据服务器id发布环境列表 39 | func (this *envService) GetEnvListByServerId(serverId int) ([]entity.Env, error) { 40 | var ( 41 | servList []entity.EnvServer 42 | envList []entity.Env 43 | ) 44 | o.QueryTable(this.serverTable()).Filter("server_id", serverId).All(&servList) 45 | envIds := make([]int, 0, len(servList)) 46 | for _, serv := range servList { 47 | envIds = append(envIds, serv.EnvId) 48 | } 49 | envList = make([]entity.Env, 0) 50 | if len(envIds) > 0 { 51 | if _, err := o.QueryTable(this.table()).Filter("id__in", envIds).All(&envList); err != nil { 52 | return envList, err 53 | } 54 | } 55 | return envList, nil 56 | } 57 | 58 | // 获取某个发布环境的服务器列表 59 | func (this *envService) GetEnvServers(envId int) ([]entity.Server, error) { 60 | var ( 61 | list []entity.EnvServer 62 | ) 63 | _, err := o.QueryTable(this.serverTable()).Filter("env_id", envId).All(&list) 64 | if err != nil { 65 | return nil, err 66 | } 67 | servIds := make([]int, 0, len(list)) 68 | for _, v := range list { 69 | servIds = append(servIds, v.ServerId) 70 | } 71 | 72 | return ServerService.GetListByIds(servIds) 73 | } 74 | 75 | // 新增发布环境 76 | func (this *envService) AddEnv(env *entity.Env) error { 77 | env.ServerCount = len(env.ServerList) 78 | if _, err := o.Insert(env); err != nil { 79 | return err 80 | } 81 | for _, sv := range env.ServerList { 82 | es := new(entity.EnvServer) 83 | es.ProjectId = env.ProjectId 84 | es.EnvId = env.Id 85 | es.ServerId = sv.Id 86 | o.Insert(es) 87 | } 88 | return nil 89 | } 90 | 91 | // 保存环境配置 92 | func (this *envService) SaveEnv(env *entity.Env) error { 93 | env.ServerCount = len(env.ServerList) 94 | if _, err := o.Update(env); err != nil { 95 | return err 96 | } 97 | o.QueryTable(this.serverTable()).Filter("env_id", env.Id).Delete() 98 | for _, sv := range env.ServerList { 99 | es := new(entity.EnvServer) 100 | es.ProjectId = env.ProjectId 101 | es.EnvId = env.Id 102 | es.ServerId = sv.Id 103 | o.Insert(es) 104 | } 105 | return nil 106 | } 107 | 108 | // 删除发布环境 109 | func (this *envService) DeleteEnv(id int) error { 110 | o.QueryTable(this.table()).Filter("id", id).Delete() 111 | o.QueryTable(this.serverTable()).Filter("env_id", id).Delete() 112 | return nil 113 | } 114 | 115 | // 删除服务器 116 | func (this *envService) DeleteServer(serverId int) error { 117 | var envServers []entity.EnvServer 118 | o.QueryTable(this.serverTable()).Filter("server_id", serverId).All(&envServers) 119 | if len(envServers) < 1 { 120 | return nil 121 | } 122 | envIds := make([]int, 0, len(envServers)) 123 | for _, v := range envServers { 124 | envIds = append(envIds, v.EnvId) 125 | } 126 | o.QueryTable(this.serverTable()).Filter("server_id", serverId).Delete() 127 | o.QueryTable(this.table()).Filter("id__in", envIds).Update(orm.Params{ 128 | "server_count": orm.ColValue(orm.ColMinus, 1), 129 | }) 130 | return nil 131 | } 132 | -------------------------------------------------------------------------------- /app/service/init.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "fmt" 5 | "github.com/astaxie/beego" 6 | "github.com/astaxie/beego/orm" 7 | _ "github.com/go-sql-driver/mysql" 8 | "github.com/lisijie/gopub/app/entity" 9 | "net/url" 10 | "os" 11 | ) 12 | 13 | var ( 14 | o orm.Ormer 15 | tablePrefix string // 表前缀 16 | UserService *userService // 用户服务 17 | RoleService *roleService // 角色服务 18 | EnvService *envService // 发布环境服务 19 | ServerService *serverService // 服务器服务 20 | ProjectService *projectService // 项目服务 21 | MailService *mailService // 邮件服务 22 | TaskService *taskService // 任务服务 23 | DeployService *deployService // 部署服务 24 | RepositoryService *repositoryService // 仓库服务 25 | SystemService *systemService 26 | ActionService *actionService // 系统动态 27 | ) 28 | 29 | func Init() { 30 | dbHost := beego.AppConfig.String("db.host") 31 | dbPort := beego.AppConfig.String("db.port") 32 | dbUser := beego.AppConfig.String("db.user") 33 | dbPassword := beego.AppConfig.String("db.password") 34 | dbName := beego.AppConfig.String("db.name") 35 | timezone := beego.AppConfig.String("db.timezone") 36 | tablePrefix = beego.AppConfig.String("db.prefix") 37 | 38 | if dbPort == "" { 39 | dbPort = "3306" 40 | } 41 | dsn := dbUser + ":" + dbPassword + "@tcp(" + dbHost + ":" + dbPort + ")/" + dbName + "?charset=utf8" 42 | if timezone != "" { 43 | dsn = dsn + "&loc=" + url.QueryEscape(timezone) 44 | } 45 | orm.RegisterDataBase("default", "mysql", dsn) 46 | 47 | orm.RegisterModelWithPrefix(tablePrefix, 48 | new(entity.Action), 49 | new(entity.Env), 50 | new(entity.EnvServer), 51 | new(entity.MailTpl), 52 | //new(entity.Perm), 53 | new(entity.Project), 54 | new(entity.Role), 55 | //new(entity.RolePerm), 56 | new(entity.Server), 57 | new(entity.Task), 58 | new(entity.TaskReview), 59 | new(entity.User), 60 | //new(entity.UserRole), 61 | ) 62 | 63 | if beego.AppConfig.String("runmode") == "dev" { 64 | orm.Debug = true 65 | } 66 | 67 | o = orm.NewOrm() 68 | orm.RunCommand() 69 | 70 | // 创建代码 71 | os.MkdirAll(GetProjectsBasePath(), 0755) 72 | os.MkdirAll(GetTasksBasePath(), 0755) 73 | 74 | // 初始化服务对象 75 | initService() 76 | } 77 | 78 | func initService() { 79 | UserService = &userService{} 80 | RoleService = &roleService{} 81 | EnvService = &envService{} 82 | ServerService = &serverService{} 83 | ProjectService = &projectService{} 84 | MailService = &mailService{} 85 | TaskService = &taskService{} 86 | DeployService = &deployService{} 87 | RepositoryService = &repositoryService{} 88 | SystemService = &systemService{} 89 | ActionService = &actionService{} 90 | } 91 | 92 | // 返回真实表名 93 | func tableName(name string) string { 94 | return tablePrefix + name 95 | } 96 | 97 | func debug(v ...interface{}) { 98 | beego.Debug(v...) 99 | } 100 | 101 | // 任务单根目录 102 | func GetTasksBasePath() string { 103 | return fmt.Sprintf(beego.AppConfig.String("data_dir") + "/tasks") 104 | } 105 | 106 | // 所有项目根目录 107 | func GetProjectsBasePath() string { 108 | return fmt.Sprintf(beego.AppConfig.String("data_dir") + "/projects") 109 | } 110 | 111 | // 任务单目录 112 | func GetTaskPath(id int) string { 113 | return fmt.Sprintf(GetTasksBasePath()+"/task-%d", id) 114 | } 115 | 116 | // 某个项目的代码目录 117 | func GetProjectPath(name string) string { 118 | return GetProjectsBasePath() + "/" + name 119 | } 120 | 121 | func concatenateError(err error, stderr string) error { 122 | if len(stderr) == 0 { 123 | return err 124 | } 125 | return fmt.Errorf("%v: %s", err, stderr) 126 | } 127 | 128 | func DBVersion() string { 129 | var lists []orm.ParamsList 130 | o.Raw("SELECT VERSION()").ValuesList(&lists) 131 | return lists[0][0].(string) 132 | } 133 | -------------------------------------------------------------------------------- /app/service/mail.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/lisijie/gopub/app/entity" 5 | ) 6 | 7 | type mailService struct{} 8 | 9 | func (this *mailService) table() string { 10 | return tableName("mail_tpl") 11 | } 12 | 13 | func (this *mailService) AddMailTpl(tpl *entity.MailTpl) error { 14 | _, err := o.Insert(tpl) 15 | return err 16 | } 17 | 18 | func (this *mailService) DelMailTpl(id int) error { 19 | _, err := o.QueryTable(this.table()).Filter("id", id).Delete() 20 | return err 21 | } 22 | 23 | func (this *mailService) SaveMailTpl(tpl *entity.MailTpl) error { 24 | _, err := o.Update(tpl) 25 | return err 26 | } 27 | 28 | func (this *mailService) GetMailTpl(id int) (*entity.MailTpl, error) { 29 | tpl := &entity.MailTpl{} 30 | tpl.Id = id 31 | err := o.Read(tpl) 32 | return tpl, err 33 | } 34 | 35 | // 获取邮件模板列表 36 | func (this *mailService) GetMailTplList() ([]entity.MailTpl, error) { 37 | var list []entity.MailTpl 38 | _, err := o.QueryTable(this.table()).OrderBy("-id").All(&list) 39 | return list, err 40 | } 41 | -------------------------------------------------------------------------------- /app/service/project.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/lisijie/gopub/app/entity" 5 | "os" 6 | ) 7 | 8 | type projectService struct{} 9 | 10 | // 表名 11 | func (this *projectService) table() string { 12 | return tableName("project") 13 | } 14 | 15 | // 获取一个项目信息 16 | func (this *projectService) GetProject(id int) (*entity.Project, error) { 17 | project := &entity.Project{} 18 | project.Id = id 19 | if err := o.Read(project); err != nil { 20 | return nil, err 21 | } 22 | return project, nil 23 | } 24 | 25 | // 获取所有项目 26 | func (this *projectService) GetAllProject() ([]entity.Project, error) { 27 | return this.GetList(1, -1) 28 | } 29 | 30 | // 获取项目列表 31 | func (this *projectService) GetList(page, pageSize int) ([]entity.Project, error) { 32 | var list []entity.Project 33 | offset := 0 34 | if pageSize == -1 { 35 | pageSize = 100000 36 | } else { 37 | offset = (page - 1) * pageSize 38 | if offset < 0 { 39 | offset = 0 40 | } 41 | } 42 | 43 | _, err := o.QueryTable(this.table()).Offset(offset).Limit(pageSize).All(&list) 44 | return list, err 45 | } 46 | 47 | // 获取项目总数 48 | func (this *projectService) GetTotal() (int64, error) { 49 | return o.QueryTable(this.table()).Count() 50 | } 51 | 52 | // 添加项目 53 | func (this *projectService) AddProject(project *entity.Project) error { 54 | _, err := o.Insert(project) 55 | return err 56 | } 57 | 58 | // 更新项目信息 59 | func (this *projectService) UpdateProject(project *entity.Project, fields ...string) error { 60 | _, err := o.Update(project, fields...) 61 | return err 62 | } 63 | 64 | // 删除一个项目 65 | func (this *projectService) DeleteProject(projectId int) error { 66 | project, err := this.GetProject(projectId) 67 | if err != nil { 68 | return err 69 | } 70 | // 删除目录 71 | path := GetProjectPath(project.Domain) 72 | os.RemoveAll(path) 73 | // 环境配置 74 | if envList, err := EnvService.GetEnvListByProjectId(project.Id); err != nil { 75 | for _, env := range envList { 76 | EnvService.DeleteEnv(env.Id) 77 | } 78 | } 79 | // 删除任务 80 | TaskService.DeleteByProjectId(project.Id) 81 | // 删除项目 82 | o.Delete(project) 83 | return nil 84 | } 85 | 86 | // 克隆某个项目的仓库 87 | func (this *projectService) CloneRepo(projectId int) error { 88 | project, err := ProjectService.GetProject(projectId) 89 | if err != nil { 90 | return err 91 | } 92 | 93 | err = RepositoryService.CloneRepo(project.RepoUrl, GetProjectPath(project.Domain)) 94 | if err != nil { 95 | project.Status = -1 96 | project.ErrorMsg = err.Error() 97 | } else { 98 | project.Status = 1 99 | } 100 | ProjectService.UpdateProject(project, "Status", "ErrorMsg") 101 | 102 | return err 103 | } 104 | -------------------------------------------------------------------------------- /app/service/role.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "errors" 5 | "github.com/lisijie/gopub/app/entity" 6 | ) 7 | 8 | type roleService struct{} 9 | 10 | func (this *roleService) table() string { 11 | return tableName("role") 12 | } 13 | 14 | // 根据id获取角色信息 15 | func (this *roleService) GetRole(id int) (*entity.Role, error) { 16 | role := &entity.Role{ 17 | Id: id, 18 | } 19 | err := o.Read(role) 20 | if err != nil { 21 | return nil, err 22 | } 23 | this.loadRoleExtra(role) 24 | return role, err 25 | } 26 | 27 | // 根据名称获取角色 28 | func (this *roleService) GetRoleByName(roleName string) (*entity.Role, error) { 29 | role := &entity.Role{ 30 | RoleName: roleName, 31 | } 32 | if err := o.Read(role, "RoleName"); err != nil { 33 | return nil, err 34 | } 35 | this.loadRoleExtra(role) 36 | return role, nil 37 | } 38 | 39 | func (this *roleService) loadRoleExtra(role *entity.Role) { 40 | o.Raw("SELECT SUBSTRING_INDEX(perm, '.', 1) as module,SUBSTRING_INDEX(perm, '.', -1) as `action`, perm AS `key` FROM "+tableName("role_perm")+" WHERE role_id = ?", role.Id).QueryRows(&role.PermList) 41 | } 42 | 43 | // 添加角色 44 | func (this *roleService) AddRole(role *entity.Role) error { 45 | if _, err := this.GetRoleByName(role.RoleName); err == nil { 46 | return errors.New("角色已存在") 47 | } 48 | _, err := o.Insert(role) 49 | return err 50 | } 51 | 52 | // 获取所有角色列表 53 | func (this *roleService) GetAllRoles() ([]entity.Role, error) { 54 | var ( 55 | roles []entity.Role // 角色列表 56 | ) 57 | if _, err := o.QueryTable(this.table()).All(&roles); err != nil { 58 | return nil, err 59 | } 60 | return roles, nil 61 | } 62 | 63 | // 更新角色信息 64 | func (this *roleService) UpdateRole(role *entity.Role, fields ...string) error { 65 | if v, err := this.GetRoleByName(role.RoleName); err == nil && v.Id != role.Id { 66 | return errors.New("角色名称已存在") 67 | } 68 | _, err := o.Update(role, fields...) 69 | return err 70 | } 71 | 72 | // 设置角色权限 73 | func (this *roleService) SetPerm(roleId int, perms []string) error { 74 | if _, err := this.GetRole(roleId); err != nil { 75 | return err 76 | } 77 | all := SystemService.GetPermList() 78 | pmmap := make(map[string]bool) 79 | for _, list := range all { 80 | for _, perm := range list { 81 | pmmap[perm.Key] = true 82 | } 83 | } 84 | for _, v := range perms { 85 | if _, ok := pmmap[v]; !ok { 86 | return errors.New("权限名称无效:" + v) 87 | } 88 | } 89 | o.Raw("DELETE FROM "+tableName("role_perm")+" WHERE role_id = ?", roleId).Exec() 90 | for _, v := range perms { 91 | o.Raw("REPLACE INTO "+tableName("role_perm")+" (role_id, perm) VALUES (?, ?)", roleId, v).Exec() 92 | } 93 | return nil 94 | } 95 | 96 | // 删除角色 97 | func (this *roleService) DeleteRole(id int) error { 98 | role, err := this.GetRole(id) 99 | if err != nil { 100 | return err 101 | } 102 | o.Delete(role) 103 | o.Raw("DELETE FROM "+tableName("role_user")+" WHERE role_id = ?", id).Exec() 104 | return nil 105 | } 106 | -------------------------------------------------------------------------------- /app/service/server.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "errors" 5 | "github.com/lisijie/gopub/app/entity" 6 | ) 7 | 8 | const ( 9 | SERVER_TYPE_NORMAL = 0 // 普通web服务器 10 | SERVER_TYPE_AGENT = 1 // 跳板机 11 | ) 12 | 13 | type serverService struct{} 14 | 15 | // 表名 16 | func (this *serverService) table() string { 17 | return tableName("server") 18 | } 19 | 20 | func (this *serverService) GetTotal(typeId int) (int64, error) { 21 | return o.QueryTable(this.table()).Filter("TypeId", typeId).Count() 22 | } 23 | 24 | // 获取一个服务器信息 25 | func (this *serverService) GetServer(id int, types ...int) (*entity.Server, error) { 26 | var err error 27 | server := &entity.Server{} 28 | server.Id = id 29 | if len(types) == 0 { 30 | err = o.Read(server) 31 | } else { 32 | err = o.QueryTable(this.table()).Filter("id", id).Filter("type_id", types[0]).One(server) 33 | } 34 | return server, err 35 | } 36 | 37 | // 根据id列表获取记录 38 | func (this *serverService) GetListByIds(ids []int) ([]entity.Server, error) { 39 | var list []entity.Server 40 | if len(ids) == 0 { 41 | return nil, errors.New("ids不能为空") 42 | } 43 | params := make([]interface{}, len(ids)) 44 | for k, v := range ids { 45 | params[k] = v 46 | } 47 | _, err := o.QueryTable(this.table()).Filter("id__in", params...).All(&list) 48 | return list, err 49 | } 50 | 51 | // 获取普通服务器列表 52 | func (this *serverService) GetServerList(page, pageSize int) ([]entity.Server, error) { 53 | var list []entity.Server 54 | qs := o.QueryTable(this.table()).Filter("TypeId", SERVER_TYPE_NORMAL) 55 | if pageSize > 0 { 56 | qs = qs.Limit(pageSize, (page-1)*pageSize) 57 | } 58 | _, err := qs.All(&list) 59 | return list, err 60 | } 61 | 62 | // 获取跳板服务器列表 63 | func (this *serverService) GetAgentList(page, pageSize int) ([]entity.Server, error) { 64 | var list []entity.Server 65 | qs := o.QueryTable(this.table()).Filter("TypeId", SERVER_TYPE_AGENT) 66 | if pageSize > 0 { 67 | qs = qs.Limit(pageSize, (page-1)*pageSize) 68 | } 69 | _, err := qs.All(&list) 70 | return list, err 71 | } 72 | 73 | // 添加服务器 74 | func (this *serverService) AddServer(server *entity.Server) error { 75 | server.Id = 0 76 | if o.Read(server, "ip"); server.Id > 0 { 77 | return errors.New("服务器IP已存在:" + server.Ip) 78 | } 79 | _, err := o.Insert(server) 80 | return err 81 | } 82 | 83 | // 修改服务器信息 84 | func (this *serverService) UpdateServer(server *entity.Server, fields ...string) error { 85 | _, err := o.Update(server, fields...) 86 | return err 87 | } 88 | 89 | // 删除服务器 90 | func (this *serverService) DeleteServer(id int) error { 91 | _, err := o.QueryTable(this.table()).Filter("id", id).Delete() 92 | if err != nil { 93 | return err 94 | } 95 | return EnvService.DeleteServer(id) 96 | } 97 | -------------------------------------------------------------------------------- /app/service/system.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/lisijie/gopub/app/entity" 5 | ) 6 | 7 | type systemService struct{} 8 | 9 | func (this *systemService) GetPermList() map[string][]entity.Perm { 10 | var list []entity.Perm 11 | o.Raw("SELECT * FROM " + tableName("perm")).QueryRows(&list) 12 | 13 | result := make(map[string][]entity.Perm) 14 | for _, v := range list { 15 | v.Key = v.Module + "." + v.Action 16 | if _, ok := result[v.Module]; !ok { 17 | result[v.Module] = make([]entity.Perm, 0) 18 | } 19 | result[v.Module] = append(result[v.Module], v) 20 | } 21 | return result 22 | } 23 | -------------------------------------------------------------------------------- /assets/avatars/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisijie/gopub/bd53895a14babdc8588e3b116dca274a6df08a55/assets/avatars/avatar.png -------------------------------------------------------------------------------- /assets/avatars/avatar1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisijie/gopub/bd53895a14babdc8588e3b116dca274a6df08a55/assets/avatars/avatar1.png -------------------------------------------------------------------------------- /assets/avatars/avatar2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisijie/gopub/bd53895a14babdc8588e3b116dca274a6df08a55/assets/avatars/avatar2.png -------------------------------------------------------------------------------- /assets/css/bootstrap-duallistbox.min.css: -------------------------------------------------------------------------------- 1 | .bootstrap-duallistbox-container .buttons{width:100%;margin-bottom:-1px}.bootstrap-duallistbox-container label{display:block}.bootstrap-duallistbox-container .info{display:inline-block;margin-bottom:5px;font-size:11px}.bootstrap-duallistbox-container .clear1,.bootstrap-duallistbox-container .clear2{display:none;font-size:10px}.bootstrap-duallistbox-container .box1.filtered .clear1,.bootstrap-duallistbox-container .box2.filtered .clear2{display:inline-block}.bootstrap-duallistbox-container .move,.bootstrap-duallistbox-container .remove{width:60%}.bootstrap-duallistbox-container .btn-group .btn{border-bottom-left-radius:0;border-bottom-right-radius:0}.bootstrap-duallistbox-container select{border-top-left-radius:0;border-top-right-radius:0}.bootstrap-duallistbox-container .moveall,.bootstrap-duallistbox-container .removeall{width:40%}.bootstrap-duallistbox-container.bs2compatible .btn-group>.btn+.btn{margin-left:0}.bootstrap-duallistbox-container select{width:100%;height:300px;padding:0}.bootstrap-duallistbox-container .filter{display:inline-block;width:100%;height:31px;margin:0 0 5px 0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.bootstrap-duallistbox-container .filter.placeholder{color:#aaa}.bootstrap-duallistbox-container.moveonselect .move,.bootstrap-duallistbox-container.moveonselect .remove{display:none}.bootstrap-duallistbox-container.moveonselect .moveall,.bootstrap-duallistbox-container.moveonselect .removeall{width:100%} -------------------------------------------------------------------------------- /assets/css/bootstrap-multiselect.min.css: -------------------------------------------------------------------------------- 1 | .multiselect-container{position:absolute;list-style-type:none;margin:0;padding:0}.multiselect-container .input-group{margin:5px}.multiselect-container>li{padding:0}.multiselect-container>li>a.multiselect-all label{font-weight:700}.multiselect-container>li.multiselect-group label{margin:0;padding:3px 20px;height:100%;font-weight:700}.multiselect-container>li.multiselect-group-clickable label{cursor:pointer}.multiselect-container>li>a{padding:0}.multiselect-container>li>a>label{margin:0;height:100%;cursor:pointer;font-weight:400;padding:3px 20px 3px 40px}.multiselect-container>li>a>label.radio,.multiselect-container>li>a>label.checkbox{margin:0}.multiselect-container>li>a>label>input[type=checkbox]{margin-bottom:5px}.filter .btn{padding:6px 3px}.btn-group>.btn-group:nth-child(2)>.multiselect.btn{border-top-left-radius:4px;border-bottom-left-radius:4px}.form-inline .multiselect-container label.checkbox,.form-inline .multiselect-container label.radio{padding:3px 20px 3px 40px}.form-inline .multiselect-container li a label.checkbox input[type=checkbox],.form-inline .multiselect-container li a label.radio input[type=radio]{margin-left:-20px;margin-right:0} -------------------------------------------------------------------------------- /assets/css/bootstrap-timepicker.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Timepicker Component for Twitter Bootstrap 3 | * 4 | * Copyright 2013 Joris de Wit 5 | * 6 | * Contributors https://github.com/jdewit/bootstrap-timepicker/graphs/contributors 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */.bootstrap-timepicker{position:relative}.bootstrap-timepicker.pull-right .bootstrap-timepicker-widget.dropdown-menu{left:auto;right:0}.bootstrap-timepicker.pull-right .bootstrap-timepicker-widget.dropdown-menu:before{left:auto;right:12px}.bootstrap-timepicker.pull-right .bootstrap-timepicker-widget.dropdown-menu:after{left:auto;right:13px}.bootstrap-timepicker .add-on{cursor:pointer}.bootstrap-timepicker .add-on i{display:inline-block;width:16px;height:16px}.bootstrap-timepicker-widget.dropdown-menu{padding:4px}.bootstrap-timepicker-widget.dropdown-menu.open{display:inline-block}.bootstrap-timepicker-widget.dropdown-menu:before{border-bottom:7px solid rgba(0,0,0,.2);border-left:7px solid transparent;border-right:7px solid transparent;content:"";display:inline-block;position:absolute}.bootstrap-timepicker-widget.dropdown-menu:after{border-bottom:6px solid #FFF;border-left:6px solid transparent;border-right:6px solid transparent;content:"";display:inline-block;position:absolute}.bootstrap-timepicker-widget.timepicker-orient-left:before{left:6px}.bootstrap-timepicker-widget.timepicker-orient-left:after{left:7px}.bootstrap-timepicker-widget.timepicker-orient-right:before{right:6px}.bootstrap-timepicker-widget.timepicker-orient-right:after{right:7px}.bootstrap-timepicker-widget.timepicker-orient-top:before{top:-7px}.bootstrap-timepicker-widget.timepicker-orient-top:after{top:-6px}.bootstrap-timepicker-widget.timepicker-orient-bottom:before{bottom:-7px;border-bottom:0;border-top:7px solid #999}.bootstrap-timepicker-widget.timepicker-orient-bottom:after{bottom:-6px;border-bottom:0;border-top:6px solid #fff}.bootstrap-timepicker-widget a.btn,.bootstrap-timepicker-widget input{border-radius:4px}.bootstrap-timepicker-widget table{width:100%;margin:0}.bootstrap-timepicker-widget table td{text-align:center;height:30px;margin:0;padding:2px}.bootstrap-timepicker-widget table td:not(.separator){min-width:30px}.bootstrap-timepicker-widget table td span{width:100%}.bootstrap-timepicker-widget table td a{border:1px transparent solid;width:100%;display:inline-block;margin:0;padding:8px 0;outline:0;color:#333}.bootstrap-timepicker-widget table td a:hover{text-decoration:none;background-color:#eee;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;border-color:#ddd}.bootstrap-timepicker-widget table td a i{margin-top:2px;font-size:18px}.bootstrap-timepicker-widget table td input{width:25px;margin:0;text-align:center}.bootstrap-timepicker-widget .modal-content{padding:4px}@media (min-width:767px){.bootstrap-timepicker-widget.modal{width:200px;margin-left:-100px}}@media (max-width:767px){.bootstrap-timepicker{width:100%}.bootstrap-timepicker .dropdown-menu{width:100%}} -------------------------------------------------------------------------------- /assets/css/chosen-sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisijie/gopub/bd53895a14babdc8588e3b116dca274a6df08a55/assets/css/chosen-sprite.png -------------------------------------------------------------------------------- /assets/css/chosen-sprite@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisijie/gopub/bd53895a14babdc8588e3b116dca274a6df08a55/assets/css/chosen-sprite@2x.png -------------------------------------------------------------------------------- /assets/css/colorbox.min.css: -------------------------------------------------------------------------------- 1 | #colorbox,#cboxOverlay,#cboxWrapper{position:absolute;top:0;left:0;z-index:9999;overflow:hidden}#cboxWrapper{max-width:none}#cboxOverlay{position:fixed;width:100%;height:100%}#cboxMiddleLeft,#cboxBottomLeft{clear:left}#cboxContent{position:relative}#cboxLoadedContent{overflow:auto;-webkit-overflow-scrolling:touch}#cboxTitle{margin:0}#cboxLoadingOverlay,#cboxLoadingGraphic{position:absolute;top:0;left:0;width:100%;height:100%}#cboxPrevious,#cboxNext,#cboxClose,#cboxSlideshow{cursor:pointer}.cboxPhoto{float:left;margin:auto;border:0;display:block;max-width:none;-ms-interpolation-mode:bicubic}.cboxIframe{width:100%;height:100%;display:block;border:0;padding:0;margin:0}#colorbox,#cboxContent,#cboxLoadedContent{box-sizing:content-box;-moz-box-sizing:content-box;-webkit-box-sizing:content-box}#cboxOverlay{background:url(images/overlay.png) repeat 0 0;opacity:.9;filter:alpha(opacity=90)}#colorbox{outline:0}#cboxTopLeft{width:21px;height:21px;background:url(images/controls.png) no-repeat -101px 0}#cboxTopRight{width:21px;height:21px;background:url(images/controls.png) no-repeat -130px 0}#cboxBottomLeft{width:21px;height:21px;background:url(images/controls.png) no-repeat -101px -29px}#cboxBottomRight{width:21px;height:21px;background:url(images/controls.png) no-repeat -130px -29px}#cboxMiddleLeft{width:21px;background:url(images/controls.png) left top repeat-y}#cboxMiddleRight{width:21px;background:url(images/controls.png) right top repeat-y}#cboxTopCenter{height:21px;background:url(images/border.png) 0 0 repeat-x}#cboxBottomCenter{height:21px;background:url(images/border.png) 0 -29px repeat-x}#cboxContent{background:#fff;overflow:hidden}.cboxIframe{background:#fff}#cboxError{padding:50px;border:1px solid #ccc}#cboxLoadedContent{margin-bottom:28px}#cboxTitle{position:absolute;bottom:4px;left:0;text-align:center;width:100%;color:#949494}#cboxCurrent{position:absolute;bottom:4px;left:58px;color:#949494}#cboxLoadingOverlay{background:url(images/loading_background.png) no-repeat center center}#cboxLoadingGraphic{background:url(images/loading.gif) no-repeat center center}#cboxPrevious,#cboxNext,#cboxSlideshow,#cboxClose{border:0;padding:0;margin:0;overflow:visible;width:auto;background:0 0}#cboxPrevious:active,#cboxNext:active,#cboxSlideshow:active,#cboxClose:active{outline:0}#cboxSlideshow{position:absolute;bottom:4px;right:30px;color:#0092ef}#cboxPrevious{position:absolute;bottom:0;left:0;background:url(images/controls.png) no-repeat -75px 0;width:25px;height:25px;text-indent:-9999px}#cboxPrevious:hover{background-position:-75px -25px}#cboxNext{position:absolute;bottom:0;left:27px;background:url(images/controls.png) no-repeat -50px 0;width:25px;height:25px;text-indent:-9999px}#cboxNext:hover{background-position:-50px -25px}#cboxClose{position:absolute;bottom:0;right:0;background:url(images/controls.png) no-repeat -25px 0;width:25px;height:25px;text-indent:-9999px}#cboxClose:hover{background-position:-25px -25px}.cboxIE #cboxTopLeft,.cboxIE #cboxTopCenter,.cboxIE #cboxTopRight,.cboxIE #cboxBottomLeft,.cboxIE #cboxBottomCenter,.cboxIE #cboxBottomRight,.cboxIE #cboxMiddleLeft,.cboxIE #cboxMiddleRight{filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#00FFFFFF, endColorstr=#00FFFFFF)} -------------------------------------------------------------------------------- /assets/css/colorpicker.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Colorpicker for Bootstrap 3 | * 4 | * Copyright 2012 Stefan Petre 5 | * Licensed under the Apache License v2.0 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | */ 9 | .colorpicker-saturation { 10 | width: 100px; 11 | height: 100px; 12 | background-image: url(img/saturation.png); 13 | cursor: crosshair; 14 | float: left; 15 | } 16 | .colorpicker-saturation i { 17 | display: block; 18 | height: 5px; 19 | width: 5px; 20 | border: 1px solid #000; 21 | -webkit-border-radius: 5px; 22 | -moz-border-radius: 5px; 23 | border-radius: 5px; 24 | position: absolute; 25 | top: 0; 26 | left: 0; 27 | margin: -4px 0 0 -4px; 28 | } 29 | .colorpicker-saturation i b { 30 | display: block; 31 | height: 5px; 32 | width: 5px; 33 | border: 1px solid #fff; 34 | -webkit-border-radius: 5px; 35 | -moz-border-radius: 5px; 36 | border-radius: 5px; 37 | } 38 | .colorpicker-hue, .colorpicker-alpha { 39 | width: 15px; 40 | height: 100px; 41 | float: left; 42 | cursor: row-resize; 43 | margin-left: 4px; 44 | margin-bottom: 4px; 45 | } 46 | .colorpicker-hue i, .colorpicker-alpha i { 47 | display: block; 48 | height: 1px; 49 | background: #000; 50 | border-top: 1px solid #fff; 51 | position: absolute; 52 | top: 0; 53 | left: 0; 54 | width: 100%; 55 | margin-top: -1px; 56 | } 57 | .colorpicker-hue { 58 | background-image: url(img/hue.png); 59 | } 60 | .colorpicker-alpha { 61 | background-image: url(img/alpha.png); 62 | display: none; 63 | } 64 | .colorpicker { 65 | *zoom: 1; 66 | top: 0; 67 | left: 0; 68 | padding: 4px; 69 | min-width: 120px; 70 | margin-top: 1px; 71 | -webkit-border-radius: 4px; 72 | -moz-border-radius: 4px; 73 | border-radius: 4px; 74 | } 75 | .colorpicker:before, .colorpicker:after { 76 | display: table; 77 | content: ""; 78 | } 79 | .colorpicker:after { 80 | clear: both; 81 | } 82 | .colorpicker:before { 83 | content: ''; 84 | display: inline-block; 85 | border-left: 7px solid transparent; 86 | border-right: 7px solid transparent; 87 | border-bottom: 7px solid #ccc; 88 | border-bottom-color: rgba(0, 0, 0, 0.2); 89 | position: absolute; 90 | top: -7px; 91 | left: 6px; 92 | } 93 | .colorpicker:after { 94 | content: ''; 95 | display: inline-block; 96 | border-left: 6px solid transparent; 97 | border-right: 6px solid transparent; 98 | border-bottom: 6px solid #ffffff; 99 | position: absolute; 100 | top: -6px; 101 | left: 7px; 102 | } 103 | .colorpicker div { 104 | position: relative; 105 | } 106 | .colorpicker.alpha { 107 | min-width: 140px; 108 | } 109 | .colorpicker.alpha .colorpicker-alpha { 110 | display: block; 111 | } 112 | .colorpicker-color { 113 | height: 10px; 114 | margin-top: 5px; 115 | clear: both; 116 | background-image: url(img/alpha.png); 117 | background-position: 0 100%; 118 | } 119 | .colorpicker-color div { 120 | height: 10px; 121 | } 122 | .input-append.color .add-on i, .input-prepend.color .add-on i { 123 | display: block; 124 | cursor: pointer; 125 | width: 16px; 126 | height: 16px; 127 | } -------------------------------------------------------------------------------- /assets/css/colorpicker.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Colorpicker for Bootstrap 3 | * 4 | * Copyright 2012 Stefan Petre 5 | * Licensed under the Apache License v2.0 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | */.colorpicker-saturation{width:100px;height:100px;background-image:url(img/saturation.png);cursor:crosshair;float:left}.colorpicker-saturation i{display:block;height:5px;width:5px;border:1px solid #000;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;position:absolute;top:0;left:0;margin:-4px 0 0 -4px}.colorpicker-saturation i b{display:block;height:5px;width:5px;border:1px solid #fff;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px}.colorpicker-hue,.colorpicker-alpha{width:15px;height:100px;float:left;cursor:row-resize;margin-left:4px;margin-bottom:4px}.colorpicker-hue i,.colorpicker-alpha i{display:block;height:1px;background:#000;border-top:1px solid #fff;position:absolute;top:0;left:0;width:100%;margin-top:-1px}.colorpicker-hue{background-image:url(img/hue.png)}.colorpicker-alpha{background-image:url(img/alpha.png);display:none}.colorpicker{*zoom:1;top:0;left:0;padding:4px;min-width:120px;margin-top:1px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.colorpicker:before,.colorpicker:after{display:table;content:""}.colorpicker:after{clear:both}.colorpicker:before{content:'';display:inline-block;border-left:7px solid transparent;border-right:7px solid transparent;border-bottom:7px solid #ccc;border-bottom-color:rgba(0,0,0,.2);position:absolute;top:-7px;left:6px}.colorpicker:after{content:'';display:inline-block;border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid #fff;position:absolute;top:-6px;left:7px}.colorpicker div{position:relative}.colorpicker.alpha{min-width:140px}.colorpicker.alpha .colorpicker-alpha{display:block}.colorpicker-color{height:10px;margin-top:5px;clear:both;background-image:url(img/alpha.png);background-position:0 100%}.colorpicker-color div{height:10px}.input-append.color .add-on i,.input-prepend.color .add-on i{display:block;cursor:pointer;width:16px;height:16px} -------------------------------------------------------------------------------- /assets/css/images/border.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisijie/gopub/bd53895a14babdc8588e3b116dca274a6df08a55/assets/css/images/border.png -------------------------------------------------------------------------------- /assets/css/images/controls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisijie/gopub/bd53895a14babdc8588e3b116dca274a6df08a55/assets/css/images/controls.png -------------------------------------------------------------------------------- /assets/css/images/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisijie/gopub/bd53895a14babdc8588e3b116dca274a6df08a55/assets/css/images/loading.gif -------------------------------------------------------------------------------- /assets/css/images/loading_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisijie/gopub/bd53895a14babdc8588e3b116dca274a6df08a55/assets/css/images/loading_background.png -------------------------------------------------------------------------------- /assets/css/images/meteorshower2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisijie/gopub/bd53895a14babdc8588e3b116dca274a6df08a55/assets/css/images/meteorshower2.jpg -------------------------------------------------------------------------------- /assets/css/images/overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisijie/gopub/bd53895a14babdc8588e3b116dca274a6df08a55/assets/css/images/overlay.png -------------------------------------------------------------------------------- /assets/css/images/pattern.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisijie/gopub/bd53895a14babdc8588e3b116dca274a6df08a55/assets/css/images/pattern.jpg -------------------------------------------------------------------------------- /assets/css/img/alpha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisijie/gopub/bd53895a14babdc8588e3b116dca274a6df08a55/assets/css/img/alpha.png -------------------------------------------------------------------------------- /assets/css/img/hue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisijie/gopub/bd53895a14babdc8588e3b116dca274a6df08a55/assets/css/img/hue.png -------------------------------------------------------------------------------- /assets/css/img/saturation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisijie/gopub/bd53895a14babdc8588e3b116dca274a6df08a55/assets/css/img/saturation.png -------------------------------------------------------------------------------- /assets/css/jquery-ui.custom.min.css: -------------------------------------------------------------------------------- 1 | /*! jQuery UI - v1.11.2 - 2014-10-28 2 | * http://jqueryui.com 3 | * Includes: core.css, draggable.css, resizable.css, selectable.css, sortable.css, slider.css 4 | * Copyright 2014 jQuery Foundation and other contributors; Licensed MIT */.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-clearfix{min-height:0}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important}.ui-icon{display:block;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-draggable-handle{-ms-touch-action:none;touch-action:none}.ui-resizable{position:relative}.ui-resizable-handle{position:absolute;font-size:.1px;display:block;-ms-touch-action:none;touch-action:none}.ui-resizable-disabled .ui-resizable-handle,.ui-resizable-autohide .ui-resizable-handle{display:none}.ui-resizable-n{cursor:n-resize;height:7px;width:100%;top:-5px;left:0}.ui-resizable-s{cursor:s-resize;height:7px;width:100%;bottom:-5px;left:0}.ui-resizable-e{cursor:e-resize;width:7px;right:-5px;top:0;height:100%}.ui-resizable-w{cursor:w-resize;width:7px;left:-5px;top:0;height:100%}.ui-resizable-se{cursor:se-resize;width:12px;height:12px;right:1px;bottom:1px}.ui-resizable-sw{cursor:sw-resize;width:9px;height:9px;left:-5px;bottom:-5px}.ui-resizable-nw{cursor:nw-resize;width:9px;height:9px;left:-5px;top:-5px}.ui-resizable-ne{cursor:ne-resize;width:9px;height:9px;right:-5px;top:-5px}.ui-selectable{-ms-touch-action:none;touch-action:none}.ui-selectable-helper{position:absolute;z-index:100;border:1px dotted #000}.ui-sortable-handle{-ms-touch-action:none;touch-action:none}.ui-slider{position:relative;text-align:left}.ui-slider .ui-slider-handle{position:absolute;z-index:2;width:1.2em;height:1.2em;cursor:default;-ms-touch-action:none;touch-action:none}.ui-slider .ui-slider-range{position:absolute;z-index:1;font-size:.7em;display:block;border:0;background-position:0 0}.ui-slider.ui-state-disabled .ui-slider-handle,.ui-slider.ui-state-disabled .ui-slider-range{filter:inherit}.ui-slider-horizontal{height:.8em}.ui-slider-horizontal .ui-slider-handle{top:-.3em;margin-left:-.6em}.ui-slider-horizontal .ui-slider-range{top:0;height:100%}.ui-slider-horizontal .ui-slider-range-min{left:0}.ui-slider-horizontal .ui-slider-range-max{right:0}.ui-slider-vertical{width:.8em;height:100px}.ui-slider-vertical .ui-slider-handle{left:-.3em;margin-left:0;margin-bottom:-.6em}.ui-slider-vertical .ui-slider-range{left:0;width:100%}.ui-slider-vertical .ui-slider-range-min{bottom:0}.ui-slider-vertical .ui-slider-range-max{top:0} -------------------------------------------------------------------------------- /assets/css/jquery.gritter.min.css: -------------------------------------------------------------------------------- 1 | #gritter-notice-wrapper{position:fixed;top:20px;right:20px;width:301px;z-index:9999}#gritter-notice-wrapper.top-left{left:20px;right:auto}#gritter-notice-wrapper.bottom-right{top:auto;left:auto;bottom:20px;right:20px}#gritter-notice-wrapper.bottom-left{top:auto;right:auto;bottom:20px;left:20px}.gritter-item-wrapper{position:relative;margin:0 0 10px 0;background:url(../images/ie-spacer.gif)}.gritter-top{background:url(../images/gritter.png) no-repeat left -30px;height:10px}.hover .gritter-top{background-position:right -30px}.gritter-bottom{background:url(../images/gritter.png) no-repeat left bottom;height:8px;margin:0}.hover .gritter-bottom{background-position:bottom right}.gritter-item{display:block;background:url(../images/gritter.png) no-repeat left -40px;color:#eee;padding:2px 11px 8px 11px;font-size:11px;font-family:verdana}.hover .gritter-item{background-position:right -40px}.gritter-item p{padding:0;margin:0;word-wrap:break-word}.gritter-close{display:none;position:absolute;top:5px;left:3px;background:url(../images/gritter.png) no-repeat left top;cursor:pointer;width:30px;height:30px}.gritter-title{font-size:14px;font-weight:700;padding:0 0 7px 0;display:block;text-shadow:1px 1px 0 #000}.gritter-image{width:48px;height:48px;float:left}.gritter-with-image,.gritter-without-image{padding:0}.gritter-with-image{width:220px;float:right}.gritter-light .gritter-item,.gritter-light .gritter-bottom,.gritter-light .gritter-top,.gritter-light .gritter-close{background-image:url(../images/gritter-light.png);color:#222}.gritter-light .gritter-title{text-shadow:none} -------------------------------------------------------------------------------- /assets/css/prettify.min.css: -------------------------------------------------------------------------------- 1 | .com{color:#93a1a1}.lit{color:#195f91}.pun,.opn,.clo{color:#93a1a1}.fun{color:#dc322f}.str,.atv{color:#D14}.kwd,.prettyprint .tag{color:#1e347b}.typ,.atn,.dec,.var{color:teal}.pln{color:#48484c}.prettyprint{padding:8px;background-color:#f7f7f9;border:1px solid #e1e1e8}.prettyprint.linenums{-webkit-box-shadow:inset 40px 0 0 #fbfbfc,inset 41px 0 0 #ececf0;box-shadow:inset 40px 0 0 #fbfbfc,inset 41px 0 0 #ececf0}ol.linenums{margin:0 0 0 33px}ol.linenums li{padding-left:12px;color:#bebec5;line-height:20px;text-shadow:0 1px 0 #fff} -------------------------------------------------------------------------------- /assets/css/select2-spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisijie/gopub/bd53895a14babdc8588e3b116dca274a6df08a55/assets/css/select2-spinner.gif -------------------------------------------------------------------------------- /assets/css/select2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisijie/gopub/bd53895a14babdc8588e3b116dca274a6df08a55/assets/css/select2.png -------------------------------------------------------------------------------- /assets/css/select2x2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisijie/gopub/bd53895a14babdc8588e3b116dca274a6df08a55/assets/css/select2x2.png -------------------------------------------------------------------------------- /assets/font-awesome/4.1.0/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisijie/gopub/bd53895a14babdc8588e3b116dca274a6df08a55/assets/font-awesome/4.1.0/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /assets/font-awesome/4.1.0/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisijie/gopub/bd53895a14babdc8588e3b116dca274a6df08a55/assets/font-awesome/4.1.0/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /assets/font-awesome/4.1.0/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisijie/gopub/bd53895a14babdc8588e3b116dca274a6df08a55/assets/font-awesome/4.1.0/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /assets/font-awesome/4.2.0/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisijie/gopub/bd53895a14babdc8588e3b116dca274a6df08a55/assets/font-awesome/4.2.0/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /assets/font-awesome/4.2.0/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisijie/gopub/bd53895a14babdc8588e3b116dca274a6df08a55/assets/font-awesome/4.2.0/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /assets/font-awesome/4.2.0/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisijie/gopub/bd53895a14babdc8588e3b116dca274a6df08a55/assets/font-awesome/4.2.0/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /assets/fonts/DXI1ORHCpsQm3Vp6mXoaTXhCUOGz7vYGh680lGh-uXM.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisijie/gopub/bd53895a14babdc8588e3b116dca274a6df08a55/assets/fonts/DXI1ORHCpsQm3Vp6mXoaTXhCUOGz7vYGh680lGh-uXM.woff -------------------------------------------------------------------------------- /assets/fonts/cJZKeOuBrn4kERxqtaUH3T8E0i7KZn-EPnyo3HZu7kw.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisijie/gopub/bd53895a14babdc8588e3b116dca274a6df08a55/assets/fonts/cJZKeOuBrn4kERxqtaUH3T8E0i7KZn-EPnyo3HZu7kw.woff -------------------------------------------------------------------------------- /assets/fonts/fonts.googleapis.com.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Open Sans'; 3 | font-style: normal; 4 | font-weight: 300; 5 | /*src: local('Open Sans Light'), local('OpenSans-Light'), url(/assets/fonts/DXI1ORHCpsQm3Vp6mXoaTXhCUOGz7vYGh680lGh-uXM.woff) format('woff');*/ 6 | } 7 | @font-face { 8 | font-family: 'Open Sans'; 9 | font-style: normal; 10 | font-weight: 400; 11 | /*src: local('Open Sans'), local('OpenSans'), url(/assets/fonts/cJZKeOuBrn4kERxqtaUH3T8E0i7KZn-EPnyo3HZu7kw.woff) format('woff');*/ 12 | } 13 | -------------------------------------------------------------------------------- /assets/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisijie/gopub/bd53895a14babdc8588e3b116dca274a6df08a55/assets/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /assets/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisijie/gopub/bd53895a14babdc8588e3b116dca274a6df08a55/assets/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /assets/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisijie/gopub/bd53895a14babdc8588e3b116dca274a6df08a55/assets/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /assets/img/clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisijie/gopub/bd53895a14babdc8588e3b116dca274a6df08a55/assets/img/clear.png -------------------------------------------------------------------------------- /assets/img/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisijie/gopub/bd53895a14babdc8588e3b116dca274a6df08a55/assets/img/loading.gif -------------------------------------------------------------------------------- /assets/js/bootstrap-tag.min.js: -------------------------------------------------------------------------------- 1 | !function(a){"use strict";var b=function(b,c){this.element=a(b),this.options=a.extend(!0,{},a.fn.tag.defaults,c),this.values=a.grep(a.map(this.element.val().split(","),a.trim),function(a){return a.length>0}),this.show()};b.prototype={constructor:b,show:function(){var b=this;b.element.parent().prepend(b.element.detach().hide()),b.element.wrap(a('
')).parent().on("click",function(){b.input.focus()}),b.values.length&&a.each(b.values,function(){b.createBadge(this)}),b.input=a('').attr("placeholder",b.options.placeholder).insertAfter(b.element).on("focus",function(){b.element.parent().addClass("tags-hover")}).on("blur",function(){b.skip||(b.process(),b.element.parent().removeClass("tags-hover"),b.element.siblings(".tag").removeClass("tag-important")),b.skip=!1}).on("keydown",function(c){if(188==c.keyCode||13==c.keyCode||9==c.keyCode)!a.trim(a(this).val())||b.element.siblings(".typeahead").length&&!b.element.siblings(".typeahead").is(":hidden")?188==c.keyCode&&(!b.element.siblings(".typeahead").length||b.element.siblings(".typeahead").is(":hidden")?c.preventDefault():(b.input.data("typeahead").select(),c.stopPropagation(),c.preventDefault())):(9!=c.keyCode&&c.preventDefault(),b.process());else if(a.trim(a(this).val())||8!=c.keyCode)b.element.siblings(".tag").removeClass("tag-important");else{var d=b.element.siblings(".tag").length;if(d){var e=b.element.siblings(".tag:eq("+(d-1)+")");e.hasClass("tag-important")?b.remove(d-1):e.addClass("tag-important")}}}).bs_typeahead({source:b.options.source,matcher:function(a){return~a.toLowerCase().indexOf(this.query.toLowerCase())&&(-1==b.inValues(a)||b.options.allowDuplicates)},updater:a.proxy(b.add,b)}),a(b.input.data("bs_typeahead").$menu).on("mousedown",function(){b.skip=!0}),this.element.trigger("shown")},inValues:function(b){if(this.options.caseInsensitive){var c=-1;return a.each(this.values,function(a,d){return d.toLowerCase()==b.toLowerCase()?(c=a,!1):void 0}),c}return a.inArray(b,this.values)},createBadge:function(b){var c=this;a("",{"class":"tag"}).text(b).append(a('').on("click",function(){c.remove(c.element.siblings(".tag").index(a(this).closest(".tag")))})).insertBefore(c.element)},add:function(b){var c=this;if(!c.options.allowDuplicates){var d=c.inValues(b);if(-1!=d){var e=c.element.siblings(".tag:eq("+d+")");return e.addClass("tag-warning"),void setTimeout(function(){a(e).removeClass("tag-warning")},500)}}this.values.push(b),this.createBadge(b),this.element.val(this.values.join(", ")),this.element.trigger("added",[b])},remove:function(a){if(a>=0){var b=this.values.splice(a,1);this.element.siblings(".tag:eq("+a+")").remove(),this.element.val(this.values.join(", ")),this.element.trigger("removed",[b])}},process:function(){var b=a.grep(a.map(this.input.val().split(","),a.trim),function(a){return a.length>0}),c=this;a.each(b,function(){c.add(this)}),this.input.val("")},skip:!1};var c=a.fn.tag;a.fn.tag=function(c){return this.each(function(){var d=a(this),e=d.data("tag"),f="object"==typeof c&&c;e||d.data("tag",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.tag.defaults={allowDuplicates:!1,caseInsensitive:!0,placeholder:"",source:[]},a.fn.tag.Constructor=b,a.fn.tag.noConflict=function(){return a.fn.tag=c,this},a(window).on("load",function(){a('[data-provide="tag"]').each(function(){var b=a(this);b.data("tag")||b.tag(b.data())})})}(window.jQuery); -------------------------------------------------------------------------------- /assets/js/bootstrap-wysiwyg.min.js: -------------------------------------------------------------------------------- 1 | !function(a){"use strict";var b=function(b){var c=a.Deferred(),d=new FileReader;return d.onload=function(a){c.resolve(a.target.result)},d.onerror=c.reject,d.onprogress=c.notify,d.readAsDataURL(b),c.promise()};a.fn.cleanHtml=function(){var b=a(this).html();return b&&b.replace(/(
|\s|

<\/div>| )*$/,"")},a.fn.wysiwyg=function(c){var d,e,f,g=this,h=function(){e.activeToolbarClass&&a(e.toolbarSelector).find(f).each(function(){try{var b=a(this).data(e.commandRole);document.queryCommandState(b)?a(this).addClass(e.activeToolbarClass):a(this).removeClass(e.activeToolbarClass)}catch(c){}})},i=function(a,b){var c=a.split(" "),d=c.shift(),e=c.join(" ")+(b||"");document.execCommand(d,0,e),h()},j=function(b){a.each(b,function(a,b){g.keydown(a,function(a){g.attr("contenteditable")&&g.is(":visible")&&(a.preventDefault(),a.stopPropagation(),i(b))}).keyup(a,function(a){g.attr("contenteditable")&&g.is(":visible")&&(a.preventDefault(),a.stopPropagation())})})},k=function(){try{var a=window.getSelection();if(a.getRangeAt&&a.rangeCount)return a.getRangeAt(0)}catch(b){}},l=function(){d=k()},m=function(){try{var a=window.getSelection();if(d){try{a.removeAllRanges()}catch(b){document.body.createTextRange().select(),document.selection.empty()}a.addRange(d)}}catch(c){}},n=function(c){g.focus(),a.each(c,function(c,d){/^image\//.test(d.type)?a.when(b(d)).done(function(a){i("insertimage",a)}).fail(function(a){e.fileUploadError("file-reader",a)}):e.fileUploadError("unsupported-file-type",d.type)})},o=function(a,b){m(),document.queryCommandSupported("hiliteColor")&&document.execCommand("hiliteColor",0,b||"transparent"),l(),a.data(e.selectionMarker,b)},p=function(b,c){b.find(f).click(function(){m(),g.focus(),i(a(this).data(c.commandRole)),l()}),b.find("[data-toggle=dropdown]").click(m);var d=!!window.navigator.msPointerEnabled||!!document.all&&!!document.addEventListener;b.find("input[type=text][data-"+c.commandRole+"]").on("webkitspeechchange change",function(){var b=this.value;this.value="",m(),b&&(g.focus(),i(a(this).data(c.commandRole),b)),l()}).on("focus",function(){if(!d){var b=a(this);b.data(c.selectionMarker)||(o(b,c.selectionColor),b.focus())}}).on("blur",function(){if(!d){var b=a(this);b.data(c.selectionMarker)&&o(b,!1)}}),b.find("input[type=file][data-"+c.commandRole+"]").change(function(){m(),"file"===this.type&&this.files&&this.files.length>0&&n(this.files),l(),this.value=""})},q=function(){g.on("dragenter dragover",!1).on("drop",function(a){var b=a.originalEvent.dataTransfer;a.stopPropagation(),a.preventDefault(),b&&b.files&&b.files.length>0&&n(b.files)})};return e=a.extend({},a.fn.wysiwyg.defaults,c),f="a[data-"+e.commandRole+"],button[data-"+e.commandRole+"],input[type=button][data-"+e.commandRole+"]",j(e.hotKeys),e.dragAndDropImages&&q(),p(a(e.toolbarSelector),e),g.attr("contenteditable",!0).on("mouseup keyup mouseout",function(){l(),h()}),a(window).bind("touchend",function(a){var b=g.is(a.target)||g.has(a.target).length>0,c=k(),d=c&&c.startContainer===c.endContainer&&c.startOffset===c.endOffset;(!d||b)&&(l(),h())}),this},a.fn.wysiwyg.defaults={hotKeys:{"ctrl+b meta+b":"bold","ctrl+i meta+i":"italic","ctrl+u meta+u":"underline","ctrl+z meta+z":"undo","ctrl+y meta+y meta+shift+z":"redo","ctrl+l meta+l":"justifyleft","ctrl+r meta+r":"justifyright","ctrl+e meta+e":"justifycenter","ctrl+j meta+j":"justifyfull","shift+tab":"outdent",tab:"indent"},toolbarSelector:"[data-role=editor-toolbar]",commandRole:"edit",activeToolbarClass:"btn-info",selectionMarker:"edit-focus-marker",selectionColor:"darkgrey",dragAndDropImages:!0,fileUploadError:function(a,b){console.log("File upload error",a,b)}}}(window.jQuery); -------------------------------------------------------------------------------- /assets/js/common.js: -------------------------------------------------------------------------------- 1 | var MSG_OK = 0; 2 | var MSG_ERR = -1; 3 | var MSG_REDIRECT = -2; 4 | 5 | jQuery(function($){ 6 | 7 | //ajax提交 8 | $('.ajax-form').on('submit', function() { 9 | var url = $('.ajax-form').attr('action') + "?_t=" + Math.random(); 10 | $('.alert').addClass('hide'); 11 | $('button[type="submit"]').attr('disabled', true); 12 | if ($('#editor')) { 13 | $("input[name='editor_content']").val($('#editor').html()); 14 | } 15 | $.post(url, $(".ajax-form").serialize(), function (out) { 16 | if (out.status == MSG_OK) { // 成功 17 | if (out.redirect != "") { 18 | window.location.href = out.redirect; 19 | } else { 20 | window.location.reload(); 21 | } 22 | } else if (out.status == MSG_REDIRECT) { 23 | window.location.href = out.redirect; 24 | } else if (out.status == MSG_ERR) { 25 | if ($('.alert')) { 26 | $('.alert').removeClass('hide'); 27 | $('.alert').html(out.msg); 28 | } else { 29 | alert(out.msg); 30 | } 31 | $('button[type="submit"]').removeAttr('disabled'); 32 | } 33 | }); 34 | return false; 35 | }); 36 | 37 | $.datepicker.regional['zh-CN'] = { 38 | closeText: '关闭', 39 | prevText: '<上月', 40 | nextText: '下月>', 41 | currentText: '今天', 42 | monthNames: ['一月','二月','三月','四月','五月','六月', 43 | '七月','八月','九月','十月','十一月','十二月'], 44 | monthNamesShort: ['一','二','三','四','五','六', 45 | '七','八','九','十','十一','十二'], 46 | dayNames: ['星期日','星期一','星期二','星期三','星期四','星期五','星期六'], 47 | dayNamesShort: ['周日','周一','周二','周三','周四','周五','周六'], 48 | dayNamesMin: ['日','一','二','三','四','五','六'], 49 | weekHeader: '周', 50 | dateFormat: 'yy-mm-dd', 51 | firstDay: 1, 52 | isRTL: false, 53 | showMonthAfterYear: true, 54 | yearSuffix: '年'}; 55 | $.datepicker.setDefaults($.datepicker.regional['zh-CN']); 56 | 57 | 58 | /** 日历控件 **/ 59 | $( "#start_date, #end_date" ).datepicker({ 60 | showOtherMonths: true, 61 | selectOtherMonths: false, 62 | dateFormat: 'yy-mm-dd', 63 | changeYear:true, 64 | changeMonth:true 65 | }); 66 | 67 | $(function () { 68 | $('[data-toggle="tooltip"]').tooltip() 69 | }) 70 | 71 | /*$('.delete').click(function () { 72 | return confirm('确定要删除这条记录吗?'); 73 | });*/ 74 | $.widget("ui.dialog", $.extend({}, $.ui.dialog.prototype, { 75 | _title: function(title) { 76 | var $title = this.options.title || ' ' 77 | if( ("title_html" in this.options) && this.options.title_html == true ) 78 | title.html($title); 79 | else title.text($title); 80 | } 81 | })); 82 | $( ".delete_confirm" ).on('click', function(e) { 83 | var del_url = $(this).attr('href'); 84 | e.preventDefault(); 85 | $( "#dialog-confirm" ).removeClass('hide').dialog({ 86 | resizable: false, 87 | width: '320', 88 | modal: true, 89 | title: "

删除确认

", 90 | title_html: true, 91 | buttons: [ 92 | { 93 | html: "  确认", 94 | "class" : "btn btn-danger btn-sm", 95 | click: function() { 96 | $( this).dialog('close'); 97 | window.location.href = del_url; 98 | } 99 | } 100 | , 101 | { 102 | html: "  取消", 103 | "class" : "btn btn-sm", 104 | click: function() { 105 | $( this ).dialog( "close" ); 106 | } 107 | } 108 | ] 109 | }); 110 | }); 111 | 112 | 113 | }); -------------------------------------------------------------------------------- /assets/js/html5shiv.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @preserve HTML5 Shiv 3.7.2 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed 3 | */ 4 | !function(a,b){function c(a,b){var c=a.createElement("p"),d=a.getElementsByTagName("head")[0]||a.documentElement;return c.innerHTML="x",d.insertBefore(c.lastChild,d.firstChild)}function d(){var a=t.elements;return"string"==typeof a?a.split(" "):a}function e(a,b){var c=t.elements;"string"!=typeof c&&(c=c.join(" ")),"string"!=typeof a&&(a=a.join(" ")),t.elements=c+" "+a,j(b)}function f(a){var b=s[a[q]];return b||(b={},r++,a[q]=r,s[r]=b),b}function g(a,c,d){if(c||(c=b),l)return c.createElement(a);d||(d=f(c));var e;return e=d.cache[a]?d.cache[a].cloneNode():p.test(a)?(d.cache[a]=d.createElem(a)).cloneNode():d.createElem(a),!e.canHaveChildren||o.test(a)||e.tagUrn?e:d.frag.appendChild(e)}function h(a,c){if(a||(a=b),l)return a.createDocumentFragment();c=c||f(a);for(var e=c.frag.cloneNode(),g=0,h=d(),i=h.length;i>g;g++)e.createElement(h[g]);return e}function i(a,b){b.cache||(b.cache={},b.createElem=a.createElement,b.createFrag=a.createDocumentFragment,b.frag=b.createFrag()),a.createElement=function(c){return t.shivMethods?g(c,a,b):b.createElem(c)},a.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+d().join().replace(/[\w\-:]+/g,function(a){return b.createElem(a),b.frag.createElement(a),'c("'+a+'")'})+");return n}")(t,b.frag)}function j(a){a||(a=b);var d=f(a);return!t.shivCSS||k||d.hasCSS||(d.hasCSS=!!c(a,"article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}template{display:none}")),l||i(a,d),a}var k,l,m="3.7.2",n=a.html5||{},o=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,p=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,q="_html5shiv",r=0,s={};!function(){try{var a=b.createElement("a");a.innerHTML="",k="hidden"in a,l=1==a.childNodes.length||function(){b.createElement("a");var a=b.createDocumentFragment();return"undefined"==typeof a.cloneNode||"undefined"==typeof a.createDocumentFragment||"undefined"==typeof a.createElement}()}catch(c){k=!0,l=!0}}();var t={elements:n.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output picture progress section summary template time video",version:m,shivCSS:n.shivCSS!==!1,supportsUnknownElements:l,shivMethods:n.shivMethods!==!1,type:"default",shivDocument:j,createElement:g,createDocumentFragment:h,addElements:e};a.html5=t,j(b)}(this,document),Function.prototype.bind||(Function.prototype.bind=function(a){if("function"!=typeof this)throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");var b=Array.prototype.slice.call(arguments,1),c=this,d=function(){},e=function(){return c.apply(this instanceof d&&a?this:a,b.concat(Array.prototype.slice.call(arguments)))};return d.prototype=this.prototype,e.prototype=new d,e}); -------------------------------------------------------------------------------- /assets/js/jquery.autosize.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | Autosize 1.18.15 3 | license: MIT 4 | http://www.jacklmoore.com/autosize 5 | */ 6 | !function(a){var b,c={className:"autosizejs",id:"autosizejs",append:"\n",callback:!1,resizeDelay:10,placeholder:!0},d=' 57 |
58 |
59 | 60 |
61 |
62 | 63 | 返回 64 |
65 |
66 | 67 | -------------------------------------------------------------------------------- /views/agent/edit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 |
7 | 8 |
9 | 10 |
11 |
12 | 13 |
14 | 15 |
16 | 17 |
18 |
19 | 20 |
21 | 22 |
23 | 24 |
25 |
26 |
27 | 28 |
29 | 30 |
31 |
32 |
33 | 34 |
35 | 36 |
37 |
38 |
39 | 40 |
41 | 42 |
43 |
密钥位于本地发布系统的路径
44 |
45 |
46 | 47 |
48 | 49 |
50 |
用于存放发布任务代码的目录,如 /home/www/projects
51 |
52 | 53 |
54 | 55 |
56 | 57 |
58 |
59 | 60 |
61 |
62 | 63 | 返回 64 |
65 |
66 |
67 | -------------------------------------------------------------------------------- /views/agent/list.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | 添加跳板机 5 |
6 |
7 | 8 |
9 | 10 |
11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | {{range $k, $v := .list}} 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 38 | 39 | {{end}} 40 | 41 |
ID服务器IP机房SSH端口SSH用户工作目录操作
{{$v.Id}}{{$v.Ip}}{{$v.Area}}{{$v.SshPort}}{{$v.SshUser}}{{$v.WorkDir}} 34 | 项目列表 | 35 | 编辑 | 36 | 删除 37 |
42 |
43 |
44 | 45 |
46 |
{{str2html .pageBar}}
47 |
48 | -------------------------------------------------------------------------------- /views/agent/projects.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 | 服务器信息:
6 |
    7 |
  • 服务器ID: {{.server.Id}}
  • 8 |
  • 服务器IP: {{.server.Ip}}
  • 9 |
  • 所在机房: {{.server.Area}}
  • 10 |
11 |
12 |
13 |
14 | 15 |
16 |

项目列表:

17 |
18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | {{range $k, $v := .list}} 28 | 29 | 30 | 31 | 32 | {{end}} 33 | 34 |
项目ID项目名称
{{$v.projectId}}{{$v.projectName}}
35 |
36 |
37 | -------------------------------------------------------------------------------- /views/env/list.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | 添加发布环境 5 |
6 |
7 | 8 |
9 | 10 |
11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | {{range $k, $v := .envList}} 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 37 | 38 | {{end}} 39 | 40 |
ID环境名称SSH帐号SSH端口发布目录服务器数量操作
{{$v.Id}}{{$v.Name}}{{$v.SshUser}}{{$v.SshPort}}{{$v.PubDir}}{{$v.ServerCount}} 34 | 编辑 | 35 | 删除 36 |
41 |
42 |
43 | -------------------------------------------------------------------------------- /views/error/message.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 |

7 | 8 | 9 | 10 | 出错了! 11 |

12 | 13 |
14 |

{{.msg}}

15 | 16 |
17 |
18 | 19 |
20 | {{if eq .redirect ""}} 21 | 22 | 23 | 返回 24 | 25 | {{else}} 26 | 27 | 28 | 返回 29 | 30 | {{end}} 31 | 32 |
33 |
34 |
35 | 36 |
37 |
-------------------------------------------------------------------------------- /views/layout/layout.html: -------------------------------------------------------------------------------- 1 | {{.Header}} 2 | 3 | {{.Navbar}} 4 | 5 |
6 | 9 | 10 | 11 | {{.Sidebar}} 12 | 13 | 14 | 15 |
16 |
17 | 31 | 32 |
33 |
34 |
35 | 36 | {{.LayoutContent}} 37 | 38 |
39 |
40 | 41 |
42 |
43 |
44 | 45 | 46 | 47 | 48 | 49 | 50 |
51 | 52 | {{.Footer}} -------------------------------------------------------------------------------- /views/layout/sections/footer.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | 一旦删除将不可恢复,你确定吗? 5 |

6 |
7 | 8 | 9 | -------------------------------------------------------------------------------- /views/layout/sections/header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 版本发布平台 v{{.version}} 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 22 | 25 | 26 | 27 | 28 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /views/layout/sections/navbar.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /views/layout/sections/sidebar.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /views/log/list.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | {{range $k, $v := .list}} 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | {{end}} 25 | 26 | 27 | 28 | 34 | 35 | 36 |
ID请求消息时间
{{$v.Id}}{{$v.Url}}{{$v.Message}}{{date $v.CreateTime "Y-m-d H:i:s"}}
29 | 33 |
37 |
38 |
39 |
40 | 41 |
42 |
{{str2html .pageBar}}
43 |
44 | 45 | -------------------------------------------------------------------------------- /views/mailtpl/list.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | 添加模板 5 |
6 |
7 | 8 |
9 | 10 |
11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {{range $k, $v := .list}} 23 | 24 | 25 | 26 | 27 | 31 | 32 | {{end}} 33 | 34 |
ID模板名称创建时间操作
{{$v.Id}}{{$v.Name}}{{date $v.CreateTime "Y-m-d H:i:s"}} 28 | 修改 | 29 | 删除 30 |
35 |
36 |
37 | -------------------------------------------------------------------------------- /views/main/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 系统登录 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 19 | 20 | 23 | 27 | 28 | 29 | 30 |
31 |
32 |
33 |
34 | 95 |
96 |
97 |
98 |
99 | 100 | 101 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /views/main/profile.html: -------------------------------------------------------------------------------- 1 | 2 | {{if .flash.error}} 3 | 6 | {{end}} 7 | {{if .flash.success}} 8 | 11 | 12 | {{end}} 13 | 14 |
15 |
16 | 17 |
18 | 19 |
20 |
21 | 22 |
23 | 24 |
25 |
26 | 30 |
31 |
32 | 36 |
37 |
38 |
39 | 40 |
41 | 42 |
43 | 44 |
45 |
46 |
47 | 48 |
49 | 50 |
51 |
52 | 不修改密码请留空 53 |
54 |
55 |
56 | 57 |
58 | 59 |
60 |
61 | 62 |
63 |
64 | 65 | 66 |
67 |
68 |
69 | -------------------------------------------------------------------------------- /views/project/add.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 |
7 | 8 |
9 | 10 |
11 |
12 | 13 |
14 | 15 |
16 | 17 |
18 |
作为本地目录名,一般为项目域名
19 |
20 | 21 |
22 | 23 |
24 | 25 |
26 |
27 |
28 | 29 |
30 | 31 |
32 |
33 | 34 |
35 |
36 | 37 |
38 | 44 |
45 |
46 |
47 | 48 |
49 | 50 |
51 |
52 |
53 | 54 |
55 | 56 |
57 |
58 | 59 |
60 |
61 | 62 |
63 |
64 | 68 |
69 |
70 | 74 |
75 |
76 |
77 |
78 | 79 |
80 | 81 |
82 |
系统将在该目录下生成2个文件version.txt和release.txt,内容分别为发布的版本号和发布时间。
83 |
84 | 85 |
86 |
87 | 88 |
89 |
90 | 94 |
95 |
96 | 100 |
101 |
102 |
103 | 104 |
105 |
106 | 107 | 返回 108 |
109 |
110 |
111 | 112 | -------------------------------------------------------------------------------- /views/project/list.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | 添加项目 5 |
6 |
7 | 8 |
9 | 10 |
11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | {{range $k, $v := .list}} 26 | 27 | 28 | 29 | 30 | 31 | 32 | 40 | 45 | 46 | {{end}} 47 | 48 | {{if eq .count 0}} 49 | 50 | 51 | 52 | {{end}} 53 | 54 |
ID项目名称项目标识仓库地址最后发布版本状态操作
{{$v.Id}}{{$v.Name}}{{$v.Domain}}{{$v.RepoUrl}}{{$v.Version}} 33 | {{if eq $v.Status 1}} 34 | 已克隆 35 | {{end}} 36 | {{if eq $v.Status -1}} 37 | 失败重新克隆 38 | {{end}} 39 | 41 | 发布环境 | 42 | 修改项目 | 43 | 删除项目 44 |
暂无记录...
55 |
56 |
57 | 58 |
59 |
{{str2html .pageBar}}
60 |
61 | 62 | -------------------------------------------------------------------------------- /views/review/detail.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |

任务信息

5 | 35 | 36 |

版本说明

37 |
{{str2html .task.Message}}
38 | 39 | 54 |

审批历史

55 | 56 |

审批人:{{.review.UserName}}

57 |

审批时间:{{date .review.CreateTime "Y-m-d H:i:s"}}

58 |

审批结果: 59 | {{if eq .review.Status 1}} 60 | 审批通过 61 | {{else if eq .review.Status -1}} 62 | 审批不通过 63 | {{end}} 64 |

65 |

审批说明:{{.review.Message}}

66 | 67 |
68 |
69 | 返回 70 |
71 |
72 | 73 |
74 |
75 | -------------------------------------------------------------------------------- /views/review/list.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 |
6 | 7 |
8 | 16 | 17 |
18 | 19 |
20 |
21 |
22 | 23 |
24 | 25 | 26 | 27 | 28 |
29 |
30 | 31 | 32 | 33 | 34 |
35 |
36 | 37 |
38 |
39 |
40 |
41 |
42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | {{range $k, $v := .list}} 58 | 59 | 60 | 61 | 62 | 68 | 69 | 70 | 79 | 86 | {{end}} 87 | 88 | {{if eq .count 0}} 89 | 90 | 91 | 92 | {{end}} 93 | 94 | 95 | 96 | 97 | 98 | 99 |
单号项目名称发布环境发布版本提单人创建时间状态操作
{{$v.Id}}{{$v.ProjectInfo.Name}}{{$v.EnvInfo.Name}} 63 | {{if ne $v.StartVer ""}} 64 | {{$v.StartVer}} - 65 | {{end}} 66 | {{$v.EndVer}} 67 | {{$v.UserName}}{{date $v.CreateTime "Y-m-d H:i:s"}} 71 | {{if eq $v.ReviewStatus 0}} 72 | 等待审批 73 | {{else if eq $v.ReviewStatus -1}} 74 | 审批不通过 75 | {{else}} 76 | 审批通过 77 | {{end}} 78 | 80 | {{if eq $v.ReviewStatus 0}} 81 | 进入审批 82 | {{else}} 83 | 浏览详情 84 | {{end}} 85 |
暂无记录...
{{str2html .pageBar}}
100 |
101 |
102 |
103 |
104 | -------------------------------------------------------------------------------- /views/review/review.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |

任务信息

5 | 35 | 36 |

版本说明

37 |
{{str2html .task.Message}}
38 | 39 | 54 |

发布审批

55 | 56 | 57 |
58 | 59 |

60 | 61 |
62 | 66 | 70 |
71 | 72 |
73 |
74 | 75 | 返回 76 |
77 |
78 |
79 | 80 |
81 |
82 | -------------------------------------------------------------------------------- /views/role/add.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 |
6 | 7 |
8 | 9 |
10 |
11 | 12 |
13 | 14 |
15 | 16 |
17 |
18 | 19 |
20 |
21 | 22 | 返回 23 |
24 |
25 |
26 | -------------------------------------------------------------------------------- /views/role/edit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 |
7 | 8 |
9 | 10 |
11 |
12 | 13 |
14 | 15 |
16 | 17 |
18 |
19 | 20 |
21 |
22 | 23 | 返回 24 |
25 |
26 |
27 | -------------------------------------------------------------------------------- /views/role/list.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 创建角色 4 |
5 |
6 | 7 |
8 | 9 |
10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {{range $k, $v := .list}} 23 | 24 | 25 | 26 | 27 | 32 | 37 | 38 | {{end}} 39 | 40 |
ID角色名称描述用户列表操作
{{$v.Id}}{{$v.RoleName}}{{$v.Description}} 28 | {{range $kk, $u := $v.UserList}} 29 | {{$u.UserName}} 30 | {{end}} 31 | 33 | 权限 | 34 | 编辑 | 35 | 删除 36 |
41 |
42 |
43 | -------------------------------------------------------------------------------- /views/role/perm.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 |
7 | 8 |
9 | 10 |
11 |
12 | 13 | 35 | 36 |
37 | 38 |
39 | 40 |
41 | {{range $key, $list := .permList}} 42 |
43 | {{range $k, $perm := $list}} 44 |
45 | 49 |
50 | {{end}} 51 | {{end}} 52 |
53 |
54 | 55 |
56 |
57 | 58 | 返回 59 |
60 |
61 |
62 | 63 | 75 | -------------------------------------------------------------------------------- /views/server/add.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 |
7 | 8 |
9 | 10 |
11 |
12 | 13 |
14 | 15 |
16 | 17 |
18 |
19 | 20 |
21 | 22 |
23 | 24 |
25 |
26 | 27 |
28 |
29 | 30 | 返回 31 |
32 |
33 |
34 | -------------------------------------------------------------------------------- /views/server/edit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 |
8 | 9 |
10 | 11 |
12 |
13 | 14 |
15 | 16 |
17 | 18 |
19 |
20 | 21 |
22 | 23 |
24 | 25 |
26 |
27 | 28 |
29 |
30 | 31 | 返回 32 |
33 |
34 |
35 | -------------------------------------------------------------------------------- /views/server/list.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | 添加服务器 5 |
6 |
7 | 8 |
9 | 10 |
11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {{range $k, $v := .list}} 23 | 24 | 25 | 26 | 27 | 32 | 33 | {{end}} 34 | 35 |
ID服务器IP所属机房操作
{{$v.Id}}{{$v.Ip}}{{$v.Area}} 28 | 项目列表 | 29 | 编辑 | 30 | 删除 31 |
36 |
37 |
38 | 39 |
40 |
{{str2html .pageBar}}
41 |
42 | -------------------------------------------------------------------------------- /views/server/projects.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 | 服务器信息:
6 |
    7 |
  • 服务器ID: {{.server.Id}}
  • 8 |
  • 服务器IP: {{.server.Ip}}
  • 9 |
  • 所在机房: {{.server.Area}}
  • 10 |
11 |
12 |
13 |
14 | 15 |
16 |

项目列表:

17 |
18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | {{range $k, $v := .list}} 29 | 30 | 31 | 32 | 33 | 34 | {{end}} 35 | 36 |
项目ID项目名称项目环境
{{$v.projectId}}{{$v.projectName}}{{$v.envName}}
37 |
38 |
39 | -------------------------------------------------------------------------------- /views/task/create.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | 8 |
9 | 10 |
11 |
12 | 16 |
17 |
18 | 22 |
23 |
24 |
25 | 26 |
27 | 28 |
29 | 31 |
32 |
33 | 34 |
35 | 36 |
37 | 39 |
40 |
41 | 42 |
43 | 44 |
45 | {{range $k,$v := .envList}} 46 | 50 |    51 | {{end}} 52 |
53 |
54 | 55 |
56 | 57 |
58 |
59 |
60 |
61 | 62 |
63 |
64 | 65 | 66 |
67 |
68 |
69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /views/task/create_step1.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | 5 |
6 | 12 |
13 |
14 | 15 |
16 | 17 |
18 |
19 | 23 |
24 |
25 |
26 | 27 | 39 | -------------------------------------------------------------------------------- /views/task/detail.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |

任务信息

5 | 40 |

版本说明

41 |
{{str2html .task.Message}}
42 |

修改日志

44 | {{if eq .task.StartVer ""}} 45 |
该版本为全量版本
46 | {{else}} 47 |
{{.task.ChangeLogs}}
48 | {{end}} 49 | 50 |

修改文件 ({{.task.GetChangeFileStat}})

51 | {{if eq .task.StartVer ""}} 52 |
该版本为全量版本
53 | {{else}} 54 |
{{.task.ChangeFiles}}
55 | {{end}} 56 | 57 | {{if gt .review.Id 0}} 58 |

审批历史

59 | 60 |

审批人:{{.review.UserName}}

61 |

审批时间:{{date .review.CreateTime "Y-m-d H:i:s"}}

62 |

审批结果: 63 | {{if eq .review.Status 1}} 64 | 审批通过 65 | {{else if eq .review.Status -1}} 66 | 审批不通过 67 | {{end}} 68 |

69 |

审批说明:{{.review.Message}}

70 | {{end}} 71 | 72 |
73 |
74 | 返回 75 |
76 |
77 | 78 |
79 |
80 | -------------------------------------------------------------------------------- /views/task/publish-step1.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 19 |
20 |
21 |
22 | 23 |
24 |
25 | 26 |

任务信息

27 | 59 | 60 |

更新文件({{.task.GetChangeFileStat}})

61 | {{if eq .task.StartVer ""}} 62 |
该版本为全量版本
63 | {{else}} 64 |
{{.task.ChangeFiles}}
65 | {{end}} 66 | 67 |
68 |
69 |
70 | 74 |
75 |
76 | 81 |
82 |
83 |
84 | 85 | 86 | -------------------------------------------------------------------------------- /views/task/publish-step2.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 19 |
20 |
21 |
22 | 23 |
24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | {{range $k, $v := .serverList}} 35 | 36 | 37 | 38 | 39 | 40 | {{end}} 41 | 42 |
服务器IP机房说明
{{$v.Ip}}{{$v.Area}}{{$v.Description}}
43 | 44 |
45 |
46 |
47 | 51 |
52 |
53 |
54 | 58 |    59 | 返回列表 60 |
61 |
62 | 63 |
64 |
错误信息:
65 |

 66 | 		
67 | 68 |
69 |
70 | 71 | 104 | -------------------------------------------------------------------------------- /views/task/publish-step3.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 19 |
20 |
21 |
22 | 23 |
24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | {{range $k, $v := .serverList}} 35 | 36 | 37 | 38 | 39 | 40 | {{end}} 41 | 42 |
服务器IP机房说明
{{$v.Ip}}{{$v.Area}}{{$v.Description}}
43 | 44 | {{if eq .task.PubStatus 3}} 45 |
发布日志:
46 |
{{.task.PubLog}}
47 |

48 | 返回列表 49 |

50 | {{end}} 51 |
52 |
53 | -------------------------------------------------------------------------------- /views/user/add.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 |
6 | 7 |
8 | 9 |
10 |
11 | 12 |
13 | 14 |
15 | 20 | 21 |
22 |
23 | 24 |
25 | 26 |
27 |
28 | 32 |
33 |
34 | 38 |
39 |
40 |
41 | 42 |
43 | 44 |
45 | 46 |
47 |
48 |
49 | 50 |
51 | 52 |
53 |
54 |
55 | 56 |
57 | 58 |
59 |
60 | 61 | 62 |
63 |
64 | 65 | 返回 66 |
67 |
68 |
69 | 70 | -------------------------------------------------------------------------------- /views/user/edit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 |
7 | 8 |
9 | 10 |
11 |
12 | 13 |
14 | 15 |
16 | 21 | 22 |
23 |
24 | 25 |
26 | 27 |
28 |
29 | 33 |
34 |
35 | 39 |
40 |
41 |
42 | 43 |
44 | 45 |
46 | 47 |
48 |
49 |
50 | 51 |
52 | 53 |
54 |
55 | 不修改密码请留空 56 |
57 |
58 |
59 | 60 |
61 | 62 |
63 |
64 | 65 |
66 | 67 |
68 |
69 | 73 |
74 |
75 | 79 |
80 |
81 |
82 | 83 |
84 |
85 | 86 | 返回 87 |
88 |
89 |
90 | 91 | -------------------------------------------------------------------------------- /views/user/list.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | 添加帐号 5 |
6 |
7 | 8 |
9 | 10 |
11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | {{range $k, $v := .list}} 26 | 27 | 28 | 29 | 30 | 31 | 36 | 44 | 48 | 49 | {{end}} 50 | 51 |
ID用户名Email最后登录时间角色状态操作
{{$v.Id}}{{$v.UserName}}{{$v.Email}}{{date $v.LastLogin "Y-m-d H:i:s"}} 32 | {{range $kk, $role := $v.RoleList}} 33 | {{$role.RoleName}} 34 | {{end}} 35 | 37 | {{if eq $v.Status 0}} 38 | 正常 39 | {{end}} 40 | {{if eq $v.Status -1}} 41 | 禁用 42 | {{end}} 43 | 45 | 修改 | 46 | 删除 47 |
52 |
53 |
54 | 55 |
56 |
{{str2html .pageBar}}
57 |
58 | --------------------------------------------------------------------------------