增加可配置的定时任务功能
This commit is contained in:
parent
5ea00d8e8a
commit
1b8ecefcfe
@ -1,6 +1,6 @@
|
|||||||
# Next Terminal
|
# Next Terminal
|
||||||
|
|
||||||
你的下一个终端。
|
下一代终端。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@ -29,6 +29,10 @@ https://next-terminal.typesafe.cn/
|
|||||||
|
|
||||||
test/test
|
test/test
|
||||||
|
|
||||||
|
## 协议与条款
|
||||||
|
|
||||||
|
如您需要在企业网络中使用 next-terminal,建议先征求 IT 管理员的同意。下载、使用或分发 next-terminal 前,您必须同意 [协议](./LICENSE) 条款与限制。本项目不提供任何担保,亦不承担任何责任。
|
||||||
|
|
||||||
## 快速安装
|
## 快速安装
|
||||||
|
|
||||||
- [使用docker安装](docs/install-docker.md)
|
- [使用docker安装](docs/install-docker.md)
|
||||||
|
43
main.go
43
main.go
@ -6,6 +6,7 @@ import (
|
|||||||
nested "github.com/antonfisher/nested-logrus-formatter"
|
nested "github.com/antonfisher/nested-logrus-formatter"
|
||||||
"github.com/labstack/gommon/log"
|
"github.com/labstack/gommon/log"
|
||||||
"github.com/patrickmn/go-cache"
|
"github.com/patrickmn/go-cache"
|
||||||
|
"github.com/robfig/cron/v3"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"gorm.io/driver/mysql"
|
"gorm.io/driver/mysql"
|
||||||
"gorm.io/driver/sqlite"
|
"gorm.io/driver/sqlite"
|
||||||
@ -155,6 +156,9 @@ func Run() error {
|
|||||||
if err := global.DB.AutoMigrate(&model.Num{}); err != nil {
|
if err := global.DB.AutoMigrate(&model.Num{}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := global.DB.AutoMigrate(&model.Job{}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if len(model.FindAllTemp()) == 0 {
|
if len(model.FindAllTemp()) == 0 {
|
||||||
for i := 0; i <= 30; i++ {
|
for i := 0; i <= 30; i++ {
|
||||||
@ -174,6 +178,45 @@ func Run() error {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
global.Store = global.NewStore()
|
global.Store = global.NewStore()
|
||||||
|
global.Cron = cron.New(cron.WithSeconds()) //精确到秒
|
||||||
|
|
||||||
|
jobs, err := model.FindJobByFunc(model.FuncCheckAssetStatusJob)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if jobs == nil || len(jobs) == 0 {
|
||||||
|
job := model.Job{
|
||||||
|
ID: utils.UUID(),
|
||||||
|
Name: "资产状态检测",
|
||||||
|
Func: model.FuncCheckAssetStatusJob,
|
||||||
|
Cron: "0 0 0/1 * * ?",
|
||||||
|
Status: model.JobStatusRunning,
|
||||||
|
Created: utils.NowJsonTime(),
|
||||||
|
Updated: utils.NowJsonTime(),
|
||||||
|
}
|
||||||
|
if err := model.CreateNewJob(&job); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
jobs, err = model.FindJobByFunc(model.FuncDelTimeoutSessionJob)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if jobs == nil || len(jobs) == 0 {
|
||||||
|
job := model.Job{
|
||||||
|
ID: utils.UUID(),
|
||||||
|
Name: "超时会话检测",
|
||||||
|
Func: model.FuncDelTimeoutSessionJob,
|
||||||
|
Cron: "0 0 0 * * ?",
|
||||||
|
Status: model.JobStatusRunning,
|
||||||
|
Created: utils.NowJsonTime(),
|
||||||
|
Updated: utils.NowJsonTime(),
|
||||||
|
}
|
||||||
|
if err := model.CreateNewJob(&job); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
loginLogs, err := model.FindAliveLoginLogs()
|
loginLogs, err := model.FindAliveLoginLogs()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
86
pkg/api/job.go
Normal file
86
pkg/api/job.go
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"next-terminal/pkg/model"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func JobCreateEndpoint(c echo.Context) error {
|
||||||
|
var item model.Job
|
||||||
|
if err := c.Bind(&item); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := model.CreateNewJob(&item); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return Success(c, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func JobPagingEndpoint(c echo.Context) error {
|
||||||
|
pageIndex, _ := strconv.Atoi(c.QueryParam("pageIndex"))
|
||||||
|
pageSize, _ := strconv.Atoi(c.QueryParam("pageSize"))
|
||||||
|
name := c.QueryParam("name")
|
||||||
|
status := c.QueryParam("status")
|
||||||
|
|
||||||
|
items, total, err := model.FindPageJob(pageIndex, pageSize, name, status)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return Success(c, H{
|
||||||
|
"total": total,
|
||||||
|
"items": items,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func JobUpdateEndpoint(c echo.Context) error {
|
||||||
|
id := c.Param("id")
|
||||||
|
|
||||||
|
var item model.Job
|
||||||
|
if err := c.Bind(&item); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := model.UpdateJobById(&item, id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return Success(c, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func JobChangeStatusEndpoint(c echo.Context) error {
|
||||||
|
id := c.Param("id")
|
||||||
|
status := c.QueryParam("status")
|
||||||
|
if err := model.ChangeJobStatusById(id, status); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return Success(c, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func JobDeleteEndpoint(c echo.Context) error {
|
||||||
|
ids := c.Param("id")
|
||||||
|
|
||||||
|
split := strings.Split(ids, ",")
|
||||||
|
for i := range split {
|
||||||
|
jobId := split[i]
|
||||||
|
if err := model.DeleteJobById(jobId); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Success(c, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func JobGetEndpoint(c echo.Context) error {
|
||||||
|
id := c.Param("id")
|
||||||
|
|
||||||
|
item, err := model.FindJobById(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return Success(c, item)
|
||||||
|
}
|
@ -144,6 +144,16 @@ func SetupRoutes() *echo.Echo {
|
|||||||
e.GET("/overview/counter", OverviewCounterEndPoint)
|
e.GET("/overview/counter", OverviewCounterEndPoint)
|
||||||
e.GET("/overview/sessions", OverviewSessionPoint)
|
e.GET("/overview/sessions", OverviewSessionPoint)
|
||||||
|
|
||||||
|
jobs := e.Group("/jobs", Admin)
|
||||||
|
{
|
||||||
|
jobs.POST("", JobCreateEndpoint)
|
||||||
|
jobs.GET("/paging", JobPagingEndpoint)
|
||||||
|
jobs.PUT("/:id", JobUpdateEndpoint)
|
||||||
|
jobs.POST("/:id/change-status", JobChangeStatusEndpoint)
|
||||||
|
jobs.DELETE("/:id", JobDeleteEndpoint)
|
||||||
|
jobs.GET("/:id", JobGetEndpoint)
|
||||||
|
}
|
||||||
|
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ package global
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/patrickmn/go-cache"
|
"github.com/patrickmn/go-cache"
|
||||||
|
"github.com/robfig/cron/v3"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"next-terminal/pkg/config"
|
"next-terminal/pkg/config"
|
||||||
)
|
)
|
||||||
@ -13,3 +14,5 @@ var Cache *cache.Cache
|
|||||||
var Config *config.Config
|
var Config *config.Config
|
||||||
|
|
||||||
var Store *TunStore
|
var Store *TunStore
|
||||||
|
|
||||||
|
var Cron *cron.Cron
|
||||||
|
@ -1,81 +1,23 @@
|
|||||||
package handle
|
package handle
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/robfig/cron/v3"
|
"next-terminal/pkg/global"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"log"
|
|
||||||
"next-terminal/pkg/guacd"
|
"next-terminal/pkg/guacd"
|
||||||
"next-terminal/pkg/model"
|
"next-terminal/pkg/model"
|
||||||
"next-terminal/pkg/utils"
|
"next-terminal/pkg/utils"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func RunTicker() {
|
func RunTicker() {
|
||||||
|
|
||||||
c := cron.New(cron.WithSeconds()) //精确到秒
|
// 每隔一小时删除一次未使用的会话信息
|
||||||
|
_, _ = global.Cron.AddJob("0 0 0/1 * * ?", model.DelUnUsedSessionJob{})
|
||||||
|
// 每隔一小时检测一次资产状态
|
||||||
|
//_, _ = global.Cron.AddJob("0 0 0/1 * * ?", model.CheckAssetStatusJob{})
|
||||||
|
// 每日凌晨删除超过时长限制的会话
|
||||||
|
//_, _ = global.Cron.AddJob("0 0 0 * * ?", model.DelTimeoutSessionJob{})
|
||||||
|
|
||||||
_, _ = c.AddFunc("0 0 0/1 * * ?", func() {
|
global.Cron.Start()
|
||||||
// 定时任务,每隔一小时删除一次未使用的会话信息
|
|
||||||
sessions, _ := model.FindSessionByStatusIn([]string{model.NoConnect, model.Connecting})
|
|
||||||
if sessions != nil && len(sessions) > 0 {
|
|
||||||
now := time.Now()
|
|
||||||
for i := range sessions {
|
|
||||||
if now.Sub(sessions[i].ConnectedTime.Time) > time.Hour*1 {
|
|
||||||
_ = model.DeleteSessionById(sessions[i].ID)
|
|
||||||
s := sessions[i].Username + "@" + sessions[i].IP + ":" + strconv.Itoa(sessions[i].Port)
|
|
||||||
logrus.Infof("会话「%v」ID「%v」超过1小时未打开,已删除。", s, sessions[i].ID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 每隔一小时检测一次资产是否存活
|
|
||||||
assets, _ := model.FindAllAsset()
|
|
||||||
if assets != nil && len(assets) > 0 {
|
|
||||||
for i := range assets {
|
|
||||||
asset := assets[i]
|
|
||||||
active := utils.Tcping(asset.IP, asset.Port)
|
|
||||||
model.UpdateAssetActiveById(active, asset.ID)
|
|
||||||
logrus.Infof("资产「%v」ID「%v」存活状态检测完成,存活「%v」。", asset.Name, asset.ID, active)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
_, err := c.AddFunc("0 0 0 * * ?", func() {
|
|
||||||
// 定时任务 每日凌晨检查超过时长限制的会话
|
|
||||||
property, err := model.FindPropertyByName("session-saved-limit")
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if property.Value == "" || property.Value == "-" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
limit, err := strconv.Atoi(property.Value)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
sessions, err := model.FindOutTimeSessions(limit)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if sessions != nil && len(sessions) > 0 {
|
|
||||||
var sessionIds []string
|
|
||||||
for i := range sessions {
|
|
||||||
sessionIds = append(sessionIds, sessions[i].ID)
|
|
||||||
}
|
|
||||||
err := model.DeleteSessionByIds(sessionIds)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Errorf("删除离线会话失败 %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Start()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func RunDataFix() {
|
func RunDataFix() {
|
||||||
|
229
pkg/model/job.go
Normal file
229
pkg/model/job.go
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"github.com/robfig/cron/v3"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"next-terminal/pkg/global"
|
||||||
|
"next-terminal/pkg/utils"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
JobStatusRunning = "running"
|
||||||
|
JobStatusNotRunning = "not-running"
|
||||||
|
|
||||||
|
FuncCheckAssetStatusJob = "check-asset-status-job"
|
||||||
|
FuncDelUnUsedSessionJob = "del-unused-session-job"
|
||||||
|
FuncDelTimeoutSessionJob = "del-timeout-session-job"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Job struct {
|
||||||
|
ID string `gorm:"primary_key" json:"id"`
|
||||||
|
JobId int `json:"jobId"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Func string `json:"func"`
|
||||||
|
Cron string `json:"cron"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Created utils.JsonTime `json:"created"`
|
||||||
|
Updated utils.JsonTime `json:"updated"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Job) TableName() string {
|
||||||
|
return "jobs"
|
||||||
|
}
|
||||||
|
|
||||||
|
func FindPageJob(pageIndex, pageSize int, name, status string) (o []Job, total int64, err error) {
|
||||||
|
job := Job{}
|
||||||
|
db := global.DB.Table(job.TableName())
|
||||||
|
dbCounter := global.DB.Table(job.TableName())
|
||||||
|
|
||||||
|
if len(name) > 0 {
|
||||||
|
db = db.Where("name like ?", "%"+name+"%")
|
||||||
|
dbCounter = dbCounter.Where("name like ?", "%"+name+"%")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(status) > 0 {
|
||||||
|
db = db.Where("status = ?", status)
|
||||||
|
dbCounter = dbCounter.Where("status = ?", status)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = dbCounter.Count(&total).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Order("created desc").Find(&o).Offset((pageIndex - 1) * pageSize).Limit(pageSize).Error
|
||||||
|
if o == nil {
|
||||||
|
o = make([]Job, 0)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func FindJobByFunc(function string) (o []Job, err error) {
|
||||||
|
db := global.DB
|
||||||
|
err = db.Where("func = ?", function).Find(&o).Error
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateNewJob(o *Job) (err error) {
|
||||||
|
|
||||||
|
if o.Status == JobStatusRunning {
|
||||||
|
j, err := getJob(o.ID, o.Func)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
jobId, err := global.Cron.AddJob(o.Cron, j)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
o.JobId = int(jobId)
|
||||||
|
}
|
||||||
|
|
||||||
|
return global.DB.Create(o).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateJobById(o *Job, id string) (err error) {
|
||||||
|
if o.Status == JobStatusRunning {
|
||||||
|
return errors.New("请先停止定时任务后再修改")
|
||||||
|
}
|
||||||
|
|
||||||
|
return global.DB.Where("id = ?", id).Updates(o).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateJonUpdatedById(id string) (err error) {
|
||||||
|
err = global.DB.Where("id = ?", id).Update("updated = ?", utils.NowJsonTime()).Error
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func ChangeJobStatusById(id, status string) (err error) {
|
||||||
|
var job Job
|
||||||
|
err = global.DB.Where("id = ?", id).First(&job).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if status == JobStatusNotRunning {
|
||||||
|
j, err := getJob(job.ID, job.Func)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
entryID, err := global.Cron.AddJob(job.Cron, j)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
job.JobId = int(entryID)
|
||||||
|
return global.DB.Where("id = ?", id).Update("status = ?", JobStatusRunning).Error
|
||||||
|
} else {
|
||||||
|
global.Cron.Remove(cron.EntryID(job.JobId))
|
||||||
|
return global.DB.Where("id = ?", id).Update("status = ?", JobStatusNotRunning).Error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func FindJobById(id string) (o Job, err error) {
|
||||||
|
err = global.DB.Where("id = ?").First(&o).Error
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteJobById(id string) error {
|
||||||
|
job, err := FindJobById(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if job.Status == JobStatusRunning {
|
||||||
|
if err := ChangeJobStatusById(JobStatusNotRunning, id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return global.DB.Where("id = ?").Delete(Job{}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func getJob(id, function string) (job cron.Job, err error) {
|
||||||
|
switch function {
|
||||||
|
case FuncCheckAssetStatusJob:
|
||||||
|
job = CheckAssetStatusJob{ID: id}
|
||||||
|
case FuncDelUnUsedSessionJob:
|
||||||
|
job = DelUnUsedSessionJob{ID: id}
|
||||||
|
case FuncDelTimeoutSessionJob:
|
||||||
|
job = DelTimeoutSessionJob{ID: id}
|
||||||
|
default:
|
||||||
|
return nil, errors.New("未识别的任务")
|
||||||
|
}
|
||||||
|
return job, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type CheckAssetStatusJob struct {
|
||||||
|
ID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r CheckAssetStatusJob) Run() {
|
||||||
|
assets, _ := FindAllAsset()
|
||||||
|
if assets != nil && len(assets) > 0 {
|
||||||
|
for i := range assets {
|
||||||
|
asset := assets[i]
|
||||||
|
active := utils.Tcping(asset.IP, asset.Port)
|
||||||
|
UpdateAssetActiveById(active, asset.ID)
|
||||||
|
logrus.Infof("资产「%v」ID「%v」存活状态检测完成,存活「%v」。", asset.Name, asset.ID, active)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if r.ID != "" {
|
||||||
|
_ = UpdateJonUpdatedById(r.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type DelUnUsedSessionJob struct {
|
||||||
|
ID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r DelUnUsedSessionJob) Run() {
|
||||||
|
sessions, _ := FindSessionByStatusIn([]string{NoConnect, Connecting})
|
||||||
|
if sessions != nil && len(sessions) > 0 {
|
||||||
|
now := time.Now()
|
||||||
|
for i := range sessions {
|
||||||
|
if now.Sub(sessions[i].ConnectedTime.Time) > time.Hour*1 {
|
||||||
|
_ = DeleteSessionById(sessions[i].ID)
|
||||||
|
s := sessions[i].Username + "@" + sessions[i].IP + ":" + strconv.Itoa(sessions[i].Port)
|
||||||
|
logrus.Infof("会话「%v」ID「%v」超过1小时未打开,已删除。", s, sessions[i].ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if r.ID != "" {
|
||||||
|
_ = UpdateJonUpdatedById(r.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type DelTimeoutSessionJob struct {
|
||||||
|
ID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r DelTimeoutSessionJob) Run() {
|
||||||
|
property, err := FindPropertyByName("session-saved-limit")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if property.Value == "" || property.Value == "-" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
limit, err := strconv.Atoi(property.Value)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sessions, err := FindOutTimeSessions(limit)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if sessions != nil && len(sessions) > 0 {
|
||||||
|
var sessionIds []string
|
||||||
|
for i := range sessions {
|
||||||
|
sessionIds = append(sessionIds, sessions[i].ID)
|
||||||
|
}
|
||||||
|
err := DeleteSessionByIds(sessionIds)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf("删除离线会话失败 %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if r.ID != "" {
|
||||||
|
_ = UpdateJonUpdatedById(r.ID)
|
||||||
|
}
|
||||||
|
}
|
@ -21,6 +21,7 @@ type User struct {
|
|||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
Created utils.JsonTime `json:"created"`
|
Created utils.JsonTime `json:"created"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
|
Mail string `json:"mail"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserVo struct {
|
type UserVo struct {
|
||||||
|
@ -16,10 +16,10 @@ import {
|
|||||||
AuditOutlined,
|
AuditOutlined,
|
||||||
BlockOutlined,
|
BlockOutlined,
|
||||||
CloudServerOutlined,
|
CloudServerOutlined,
|
||||||
CodeOutlined,
|
CodeOutlined, ControlOutlined,
|
||||||
DashboardOutlined,
|
DashboardOutlined,
|
||||||
DesktopOutlined,
|
DesktopOutlined,
|
||||||
DisconnectOutlined,
|
DisconnectOutlined, GoldOutlined,
|
||||||
IdcardOutlined,
|
IdcardOutlined,
|
||||||
LinkOutlined,
|
LinkOutlined,
|
||||||
LoginOutlined,
|
LoginOutlined,
|
||||||
@ -39,6 +39,7 @@ import {isAdmin} from "./service/permission";
|
|||||||
import UserGroup from "./components/user/UserGroup";
|
import UserGroup from "./components/user/UserGroup";
|
||||||
import LoginLog from "./components/session/LoginLog";
|
import LoginLog from "./components/session/LoginLog";
|
||||||
import Term from "./components/access/Term";
|
import Term from "./components/access/Term";
|
||||||
|
import Job from "./components/job/Job";
|
||||||
|
|
||||||
const {Footer, Sider} = Layout;
|
const {Footer, Sider} = Layout;
|
||||||
|
|
||||||
@ -161,23 +162,10 @@ class App extends Component {
|
|||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</SubMenu>
|
</SubMenu>
|
||||||
|
|
||||||
<SubMenu key='command-manage' title='指令管理' icon={<CodeOutlined/>}>
|
|
||||||
<Menu.Item key="dynamic-command" icon={<BlockOutlined/>}>
|
|
||||||
<Link to={'/dynamic-command'}>
|
|
||||||
动态指令
|
|
||||||
</Link>
|
|
||||||
</Menu.Item>
|
|
||||||
{/*<Menu.Item key="silent-command" icon={<DeploymentUnitOutlined/>}>
|
|
||||||
<Link to={'/silent-command'}>
|
|
||||||
静默指令
|
|
||||||
</Link>
|
|
||||||
</Menu.Item>*/}
|
|
||||||
</SubMenu>
|
|
||||||
|
|
||||||
{
|
{
|
||||||
this.state.triggerMenu && isAdmin() ?
|
this.state.triggerMenu && isAdmin() ?
|
||||||
<>
|
<>
|
||||||
<SubMenu key='audit' title='操作审计' icon={<AuditOutlined/>}>
|
<SubMenu key='audit' title='会话审计' icon={<AuditOutlined/>}>
|
||||||
<Menu.Item key="online-session" icon={<LinkOutlined/>}>
|
<Menu.Item key="online-session" icon={<LinkOutlined/>}>
|
||||||
<Link to={'/online-session'}>
|
<Link to={'/online-session'}>
|
||||||
在线会话
|
在线会话
|
||||||
@ -195,6 +183,28 @@ class App extends Component {
|
|||||||
登录日志
|
登录日志
|
||||||
</Link>
|
</Link>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
|
||||||
|
</SubMenu>
|
||||||
|
|
||||||
|
<SubMenu key='ops' title='系统运维' icon={<ControlOutlined />}>
|
||||||
|
|
||||||
|
<Menu.Item key="dynamic-command" icon={<CodeOutlined/>}>
|
||||||
|
<Link to={'/dynamic-command'}>
|
||||||
|
动态指令
|
||||||
|
</Link>
|
||||||
|
</Menu.Item>
|
||||||
|
|
||||||
|
{/*<Menu.Item key="silent-command" icon={<DeploymentUnitOutlined/>}>
|
||||||
|
<Link to={'/silent-command'}>
|
||||||
|
静默指令
|
||||||
|
</Link>
|
||||||
|
</Menu.Item>*/}
|
||||||
|
|
||||||
|
<Menu.Item key="job" icon={<BlockOutlined />}>
|
||||||
|
<Link to={'/job'}>
|
||||||
|
定时任务
|
||||||
|
</Link>
|
||||||
|
</Menu.Item>
|
||||||
</SubMenu>
|
</SubMenu>
|
||||||
|
|
||||||
<SubMenu key='user-group' title='用户管理' icon={<UserSwitchOutlined/>}>
|
<SubMenu key='user-group' title='用户管理' icon={<UserSwitchOutlined/>}>
|
||||||
@ -251,6 +261,7 @@ class App extends Component {
|
|||||||
<Route path="/login-log" component={LoginLog}/>
|
<Route path="/login-log" component={LoginLog}/>
|
||||||
<Route path="/info" component={Info}/>
|
<Route path="/info" component={Info}/>
|
||||||
<Route path="/setting" component={Setting}/>
|
<Route path="/setting" component={Setting}/>
|
||||||
|
<Route path="/job" component={Job}/>
|
||||||
|
|
||||||
<Footer style={{textAlign: 'center'}}>
|
<Footer style={{textAlign: 'center'}}>
|
||||||
Next Terminal ©2021 dushixiang Version:{this.state.package['version']}
|
Next Terminal ©2021 dushixiang Version:{this.state.package['version']}
|
||||||
|
450
web/src/components/job/Job.js
Normal file
450
web/src/components/job/Job.js
Normal file
@ -0,0 +1,450 @@
|
|||||||
|
import React, {Component} from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
Col,
|
||||||
|
Divider,
|
||||||
|
Dropdown,
|
||||||
|
Input,
|
||||||
|
Layout,
|
||||||
|
Menu,
|
||||||
|
Modal,
|
||||||
|
PageHeader,
|
||||||
|
Row,
|
||||||
|
Space,
|
||||||
|
Table,
|
||||||
|
Tooltip,
|
||||||
|
Typography
|
||||||
|
} from "antd";
|
||||||
|
import qs from "qs";
|
||||||
|
import request from "../../common/request";
|
||||||
|
import {message} from "antd/es";
|
||||||
|
import {
|
||||||
|
DeleteOutlined,
|
||||||
|
DownOutlined,
|
||||||
|
ExclamationCircleOutlined,
|
||||||
|
PlusOutlined,
|
||||||
|
SyncOutlined,
|
||||||
|
UndoOutlined
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import {itemRender} from "../../utils/utils";
|
||||||
|
import Logout from "../user/Logout";
|
||||||
|
import {hasPermission} from "../../service/permission";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
|
const confirm = Modal.confirm;
|
||||||
|
const {Content} = Layout;
|
||||||
|
const {Title, Text} = Typography;
|
||||||
|
const {Search} = Input;
|
||||||
|
const CheckboxGroup = Checkbox.Group;
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
breadcrumbName: '首页',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'job',
|
||||||
|
breadcrumbName: '定时任务',
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
class Job extends Component {
|
||||||
|
|
||||||
|
inputRefOfName = React.createRef();
|
||||||
|
|
||||||
|
state = {
|
||||||
|
items: [],
|
||||||
|
total: 0,
|
||||||
|
queryParams: {
|
||||||
|
pageIndex: 1,
|
||||||
|
pageSize: 10
|
||||||
|
},
|
||||||
|
loading: false,
|
||||||
|
modalVisible: false,
|
||||||
|
modalTitle: '',
|
||||||
|
modalConfirmLoading: false,
|
||||||
|
selectedRowKeys: []
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.loadTableData();
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id) {
|
||||||
|
const result = await request.delete('/jobs/' + id);
|
||||||
|
if (result.code === 1) {
|
||||||
|
message.success('删除成功');
|
||||||
|
this.loadTableData(this.state.queryParams);
|
||||||
|
} else {
|
||||||
|
message.error('删除失败 :( ' + result.message, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadTableData(queryParams) {
|
||||||
|
this.setState({
|
||||||
|
loading: true
|
||||||
|
});
|
||||||
|
|
||||||
|
queryParams = queryParams || this.state.queryParams;
|
||||||
|
|
||||||
|
// queryParams
|
||||||
|
let paramsStr = qs.stringify(queryParams);
|
||||||
|
|
||||||
|
let data = {
|
||||||
|
items: [],
|
||||||
|
total: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
let result = await request.get('/jobs/paging?' + paramsStr);
|
||||||
|
if (result.code === 1) {
|
||||||
|
data = result.data;
|
||||||
|
} else {
|
||||||
|
message.error(result.message);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
const items = data.items.map(item => {
|
||||||
|
return {'key': item['id'], ...item}
|
||||||
|
})
|
||||||
|
this.setState({
|
||||||
|
items: items,
|
||||||
|
total: data.total,
|
||||||
|
queryParams: queryParams,
|
||||||
|
loading: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChangPage = async (pageIndex, pageSize) => {
|
||||||
|
let queryParams = this.state.queryParams;
|
||||||
|
queryParams.pageIndex = pageIndex;
|
||||||
|
queryParams.pageSize = pageSize;
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
queryParams: queryParams
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.loadTableData(queryParams)
|
||||||
|
};
|
||||||
|
|
||||||
|
handleSearchByName = name => {
|
||||||
|
let query = {
|
||||||
|
...this.state.queryParams,
|
||||||
|
'pageIndex': 1,
|
||||||
|
'pageSize': this.state.queryParams.pageSize,
|
||||||
|
'name': name,
|
||||||
|
}
|
||||||
|
this.loadTableData(query);
|
||||||
|
};
|
||||||
|
|
||||||
|
showDeleteConfirm(id, content) {
|
||||||
|
let self = this;
|
||||||
|
confirm({
|
||||||
|
title: '您确定要删除此任务吗?',
|
||||||
|
content: content,
|
||||||
|
okText: '确定',
|
||||||
|
okType: 'danger',
|
||||||
|
cancelText: '取消',
|
||||||
|
onOk() {
|
||||||
|
self.delete(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
showModal(title, assets = null) {
|
||||||
|
this.setState({
|
||||||
|
modalTitle: title,
|
||||||
|
modalVisible: true,
|
||||||
|
model: assets
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
handleCancelModal = e => {
|
||||||
|
this.setState({
|
||||||
|
modalTitle: '',
|
||||||
|
modalVisible: false
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
handleOk = async (formData) => {
|
||||||
|
// 弹窗 form 传来的数据
|
||||||
|
this.setState({
|
||||||
|
modalConfirmLoading: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (formData.id) {
|
||||||
|
// 向后台提交数据
|
||||||
|
const result = await request.put('/jobs/' + formData.id, formData);
|
||||||
|
if (result.code === 1) {
|
||||||
|
message.success('更新成功');
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
modalVisible: false
|
||||||
|
});
|
||||||
|
this.loadTableData(this.state.queryParams);
|
||||||
|
} else {
|
||||||
|
message.error('更新失败 :( ' + result.message, 10);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 向后台提交数据
|
||||||
|
const result = await request.post('/jobs', formData);
|
||||||
|
if (result.code === 1) {
|
||||||
|
message.success('新增成功');
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
modalVisible: false
|
||||||
|
});
|
||||||
|
this.loadTableData(this.state.queryParams);
|
||||||
|
} else {
|
||||||
|
message.error('新增失败 :( ' + result.message, 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
modalConfirmLoading: false
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
batchDelete = async () => {
|
||||||
|
this.setState({
|
||||||
|
delBtnLoading: true
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
let result = await request.delete('/jobs/' + this.state.selectedRowKeys.join(','));
|
||||||
|
if (result.code === 1) {
|
||||||
|
message.success('操作成功', 3);
|
||||||
|
this.setState({
|
||||||
|
selectedRowKeys: []
|
||||||
|
})
|
||||||
|
await this.loadTableData(this.state.queryParams);
|
||||||
|
} else {
|
||||||
|
message.error('删除失败 :( ' + result.message, 10);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.setState({
|
||||||
|
delBtnLoading: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
|
||||||
|
const columns = [{
|
||||||
|
title: '序号',
|
||||||
|
dataIndex: 'id',
|
||||||
|
key: 'id',
|
||||||
|
render: (id, record, index) => {
|
||||||
|
return index + 1;
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
title: '任务名称',
|
||||||
|
dataIndex: 'name',
|
||||||
|
key: 'name',
|
||||||
|
render: (name, record) => {
|
||||||
|
let short = name;
|
||||||
|
if (short && short.length > 20) {
|
||||||
|
short = short.substring(0, 20) + " ...";
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Tooltip placement="topLeft" title={name}>
|
||||||
|
{short}
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
title: '任务类型',
|
||||||
|
dataIndex: 'func',
|
||||||
|
key: 'func'
|
||||||
|
}, {
|
||||||
|
title: 'cron表达式',
|
||||||
|
dataIndex: 'cron',
|
||||||
|
key: 'cron'
|
||||||
|
}, {
|
||||||
|
title: '创建日期',
|
||||||
|
dataIndex: 'created',
|
||||||
|
key: 'created',
|
||||||
|
render: (text, record) => {
|
||||||
|
return (
|
||||||
|
<Tooltip title={text}>
|
||||||
|
{dayjs(text).fromNow()}
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
title: '更新日期',
|
||||||
|
dataIndex: 'updated',
|
||||||
|
key: 'updated',
|
||||||
|
render: (text, record) => {
|
||||||
|
return (
|
||||||
|
<Tooltip title={text}>
|
||||||
|
{dayjs(text).fromNow()}
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
render: (text, record) => {
|
||||||
|
|
||||||
|
const menu = (
|
||||||
|
<Menu>
|
||||||
|
<Menu.Item key="0">
|
||||||
|
<Button type="text" size='small'
|
||||||
|
disabled={!hasPermission(record['owner'])}
|
||||||
|
onClick={() => this.showModal('更新指令', record)}>编辑</Button>
|
||||||
|
</Menu.Item>
|
||||||
|
|
||||||
|
<Menu.Divider/>
|
||||||
|
<Menu.Item key="3">
|
||||||
|
<Button type="text" size='small' danger
|
||||||
|
disabled={!hasPermission(record['owner'])}
|
||||||
|
onClick={() => this.showDeleteConfirm(record.id, record.name)}>删除</Button>
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Button type="link" size='small' onClick={async () => {
|
||||||
|
|
||||||
|
}}>执行</Button>
|
||||||
|
|
||||||
|
<Dropdown overlay={menu}>
|
||||||
|
<Button type="link" size='small'>
|
||||||
|
更多 <DownOutlined/>
|
||||||
|
</Button>
|
||||||
|
</Dropdown>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const selectedRowKeys = this.state.selectedRowKeys;
|
||||||
|
const rowSelection = {
|
||||||
|
selectedRowKeys: this.state.selectedRowKeys,
|
||||||
|
onChange: (selectedRowKeys, selectedRows) => {
|
||||||
|
this.setState({selectedRowKeys});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const hasSelected = selectedRowKeys.length > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHeader
|
||||||
|
className="site-page-header-ghost-wrapper page-herder"
|
||||||
|
title="定时任务"
|
||||||
|
breadcrumb={{
|
||||||
|
routes: routes,
|
||||||
|
itemRender: itemRender
|
||||||
|
}}
|
||||||
|
extra={[
|
||||||
|
<Logout key='logout'/>
|
||||||
|
]}
|
||||||
|
subTitle="定时任务"
|
||||||
|
>
|
||||||
|
</PageHeader>
|
||||||
|
|
||||||
|
<Content className="site-layout-background page-content">
|
||||||
|
|
||||||
|
<div style={{marginBottom: 20}}>
|
||||||
|
<Row justify="space-around" align="middle" gutter={24}>
|
||||||
|
<Col span={12} key={1}>
|
||||||
|
<Title level={3}>任务列表</Title>
|
||||||
|
</Col>
|
||||||
|
<Col span={12} key={2} style={{textAlign: 'right'}}>
|
||||||
|
<Space>
|
||||||
|
<Search
|
||||||
|
ref={this.inputRefOfName}
|
||||||
|
placeholder="任务名称"
|
||||||
|
allowClear
|
||||||
|
onSearch={this.handleSearchByName}
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
<Tooltip title='重置查询'>
|
||||||
|
|
||||||
|
<Button icon={<UndoOutlined/>} onClick={() => {
|
||||||
|
this.inputRefOfName.current.setValue('');
|
||||||
|
this.loadTableData({pageIndex: 1, pageSize: 10, name: '', content: ''})
|
||||||
|
}}>
|
||||||
|
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Divider type="vertical"/>
|
||||||
|
|
||||||
|
<Tooltip title="新增">
|
||||||
|
<Button type="dashed" icon={<PlusOutlined/>}
|
||||||
|
onClick={() => this.showModal('新增任务', {})}>
|
||||||
|
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
|
||||||
|
<Tooltip title="刷新列表">
|
||||||
|
<Button icon={<SyncOutlined/>} onClick={() => {
|
||||||
|
this.loadTableData(this.state.queryParams)
|
||||||
|
}}>
|
||||||
|
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
|
||||||
|
<Tooltip title="批量删除">
|
||||||
|
<Button type="primary" danger disabled={!hasSelected} icon={<DeleteOutlined/>}
|
||||||
|
loading={this.state.delBtnLoading}
|
||||||
|
onClick={() => {
|
||||||
|
const content = <div>
|
||||||
|
您确定要删除选中的<Text style={{color: '#1890FF'}}
|
||||||
|
strong>{this.state.selectedRowKeys.length}</Text>条记录吗?
|
||||||
|
</div>;
|
||||||
|
confirm({
|
||||||
|
icon: <ExclamationCircleOutlined/>,
|
||||||
|
content: content,
|
||||||
|
onOk: () => {
|
||||||
|
this.batchDelete()
|
||||||
|
},
|
||||||
|
onCancel() {
|
||||||
|
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}>
|
||||||
|
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
</Space>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Table
|
||||||
|
rowSelection={rowSelection}
|
||||||
|
dataSource={this.state.items}
|
||||||
|
columns={columns}
|
||||||
|
position={'both'}
|
||||||
|
pagination={{
|
||||||
|
showSizeChanger: true,
|
||||||
|
current: this.state.queryParams.pageIndex,
|
||||||
|
pageSize: this.state.queryParams.pageSize,
|
||||||
|
onChange: this.handleChangPage,
|
||||||
|
onShowSizeChange: this.handleChangPage,
|
||||||
|
total: this.state.total,
|
||||||
|
showTotal: total => `总计 ${total} 条`
|
||||||
|
}}
|
||||||
|
loading={this.state.loading}
|
||||||
|
/>
|
||||||
|
|
||||||
|
</Content>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Job;
|
Loading…
Reference in New Issue
Block a user