feat: 计划任务支持多周期执行 (#3676)

This commit is contained in:
ssongliu 2024-01-22 22:29:19 +08:00 committed by GitHub
parent 64def5bebd
commit 115b9c58b0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 523 additions and 475 deletions

View File

@ -3,14 +3,9 @@ package dto
import "time"
type CronjobCreate struct {
Name string `json:"name" validate:"required"`
Type string `json:"type" validate:"required"`
SpecType string `json:"specType" validate:"required"`
Week int `json:"week" validate:"number,max=6,min=0"`
Day int `json:"day" validate:"number"`
Hour int `json:"hour" validate:"number"`
Minute int `json:"minute" validate:"number"`
Second int `json:"second" validate:"number"`
Name string `json:"name" validate:"required"`
Type string `json:"type" validate:"required"`
Spec string `json:"spec" validate:"required"`
Script string `json:"script"`
ContainerName string `json:"containerName"`
@ -27,14 +22,9 @@ type CronjobCreate struct {
}
type CronjobUpdate struct {
ID uint `json:"id" validate:"required"`
Name string `json:"name" validate:"required"`
SpecType string `json:"specType" validate:"required"`
Week int `json:"week" validate:"number,max=6,min=0"`
Day int `json:"day" validate:"number"`
Hour int `json:"hour" validate:"number"`
Minute int `json:"minute" validate:"number"`
Second int `json:"second" validate:"number"`
ID uint `json:"id" validate:"required"`
Name string `json:"name" validate:"required"`
Spec string `json:"spec" validate:"required"`
Script string `json:"script"`
ContainerName string `json:"containerName"`
@ -71,15 +61,10 @@ type CronjobBatchDelete struct {
}
type CronjobInfo struct {
ID uint `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
SpecType string `json:"specType"`
Week int `json:"week"`
Day int `json:"day"`
Hour int `json:"hour"`
Minute int `json:"minute"`
Second int `json:"second"`
ID uint `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Spec string `json:"spec"`
Script string `json:"script"`
ContainerName string `json:"containerName"`

View File

@ -5,15 +5,9 @@ import "time"
type Cronjob struct {
BaseModel
Name string `gorm:"type:varchar(64);not null;unique" json:"name"`
Type string `gorm:"type:varchar(64);not null" json:"type"`
SpecType string `gorm:"type:varchar(64);not null" json:"specType"`
Spec string `gorm:"type:varchar(64);not null" json:"spec"`
Week uint64 `gorm:"type:decimal" json:"week"`
Day uint64 `gorm:"type:decimal" json:"day"`
Hour uint64 `gorm:"type:decimal" json:"hour"`
Minute uint64 `gorm:"type:decimal" json:"minute"`
Second uint64 `gorm:"type:decimal" json:"second"`
Name string `gorm:"type:varchar(64);not null" json:"name"`
Type string `gorm:"type:varchar(64);not null" json:"type"`
Spec string `gorm:"type:varchar(64);not null" json:"spec"`
ContainerName string `gorm:"type:varchar(64)" json:"containerName"`
Script string `gorm:"longtext" json:"script"`
@ -29,9 +23,9 @@ type Cronjob struct {
TargetDirID uint64 `gorm:"type:decimal" json:"targetDirID"`
RetainCopies uint64 `gorm:"type:decimal" json:"retainCopies"`
Status string `gorm:"type:varchar(64)" json:"status"`
EntryID uint64 `gorm:"type:decimal" json:"entryID"`
Records []JobRecords `json:"records"`
Status string `gorm:"type:varchar(64)" json:"status"`
EntryIDs string `gorm:"type:varchar(64)" json:"entryIDs"`
Records []JobRecords `json:"records"`
}
type JobRecords struct {

View File

@ -5,6 +5,8 @@ import (
"fmt"
"os"
"path"
"strconv"
"strings"
"time"
"github.com/1Panel-dev/1Panel/backend/app/dto"
@ -28,7 +30,7 @@ type ICronjobService interface {
UpdateStatus(id uint, status string) error
Delete(req dto.CronjobBatchDelete) error
Download(down dto.CronjobDownload) (string, error)
StartJob(cronjob *model.Cronjob) (int, error)
StartJob(cronjob *model.Cronjob) (string, error)
CleanRecord(req dto.CronjobClean) error
LoadRecordLog(req dto.OperateByID) (string, error)
@ -184,29 +186,40 @@ func (u *CronjobService) Create(cronjobDto dto.CronjobCreate) error {
return errors.WithMessage(constant.ErrStructTransform, err.Error())
}
cronjob.Status = constant.StatusEnable
cronjob.Spec = loadSpec(cronjob)
global.LOG.Infof("create cronjob %s successful, spec: %s", cronjob.Name, cronjob.Spec)
entryID, err := u.StartJob(&cronjob)
spec := cronjob.Spec
entryIDs, err := u.StartJob(&cronjob)
if err != nil {
return err
}
cronjob.EntryID = uint64(entryID)
cronjob.Spec = spec
cronjob.EntryIDs = entryIDs
if err := cronjobRepo.Create(&cronjob); err != nil {
return err
}
return nil
}
func (u *CronjobService) StartJob(cronjob *model.Cronjob) (int, error) {
if cronjob.EntryID != 0 {
global.Cron.Remove(cron.EntryID(cronjob.EntryID))
func (u *CronjobService) StartJob(cronjob *model.Cronjob) (string, error) {
if len(cronjob.EntryIDs) != 0 {
ids := strings.Split(cronjob.EntryIDs, ",")
for _, id := range ids {
idItem, _ := strconv.Atoi(id)
global.Cron.Remove(cron.EntryID(idItem))
}
}
entryID, err := u.AddCronJob(cronjob)
if err != nil {
return 0, err
specs := strings.Split(cronjob.Spec, ",")
var ids []string
for _, spec := range specs {
cronjob.Spec = spec
entryID, err := u.AddCronJob(cronjob)
if err != nil {
return "", err
}
ids = append(ids, fmt.Sprintf("%v", entryID))
}
return entryID, nil
return strings.Join(ids, ","), nil
}
func (u *CronjobService) Delete(req dto.CronjobBatchDelete) error {
@ -215,8 +228,12 @@ func (u *CronjobService) Delete(req dto.CronjobBatchDelete) error {
if cronjob.ID == 0 {
return errors.New("find cronjob in db failed")
}
global.Cron.Remove(cron.EntryID(cronjob.EntryID))
global.LOG.Infof("stop cronjob entryID: %d", cronjob.EntryID)
ids := strings.Split(cronjob.EntryIDs, ",")
for _, id := range ids {
idItem, _ := strconv.Atoi(id)
global.Cron.Remove(cron.EntryID(idItem))
}
global.LOG.Infof("stop cronjob entryID: %s", cronjob.EntryIDs)
if err := u.CleanRecord(dto.CronjobClean{CronjobID: id, CleanData: req.CleanData}); err != nil {
return err
}
@ -238,29 +255,27 @@ func (u *CronjobService) Update(id uint, req dto.CronjobUpdate) error {
return constant.ErrRecordNotFound
}
upMap := make(map[string]interface{})
cronjob.EntryID = cronModel.EntryID
cronjob.EntryIDs = cronModel.EntryIDs
cronjob.Type = cronModel.Type
cronjob.Spec = loadSpec(cronjob)
spec := cronjob.Spec
if cronModel.Status == constant.StatusEnable {
newEntryID, err := u.StartJob(&cronjob)
newEntryIDs, err := u.StartJob(&cronjob)
if err != nil {
return err
}
upMap["entry_id"] = newEntryID
upMap["entry_ids"] = newEntryIDs
} else {
global.Cron.Remove(cron.EntryID(cronjob.EntryID))
ids := strings.Split(cronjob.EntryIDs, ",")
for _, id := range ids {
idItem, _ := strconv.Atoi(id)
global.Cron.Remove(cron.EntryID(idItem))
}
}
upMap["name"] = req.Name
upMap["spec"] = cronjob.Spec
upMap["spec"] = spec
upMap["script"] = req.Script
upMap["container_name"] = req.ContainerName
upMap["spec_type"] = req.SpecType
upMap["week"] = req.Week
upMap["day"] = req.Day
upMap["hour"] = req.Hour
upMap["minute"] = req.Minute
upMap["second"] = req.Second
upMap["app_id"] = req.AppID
upMap["website"] = req.Website
upMap["exclusion_rules"] = req.ExclusionRules
@ -280,19 +295,23 @@ func (u *CronjobService) UpdateStatus(id uint, status string) error {
return errors.WithMessage(constant.ErrRecordNotFound, "record not found")
}
var (
entryID int
err error
entryIDs string
err error
)
if status == constant.StatusEnable {
entryID, err = u.StartJob(&cronjob)
entryIDs, err = u.StartJob(&cronjob)
if err != nil {
return err
}
} else {
global.Cron.Remove(cron.EntryID(cronjob.EntryID))
global.LOG.Infof("stop cronjob entryID: %d", cronjob.EntryID)
ids := strings.Split(cronjob.EntryIDs, ",")
for _, id := range ids {
idItem, _ := strconv.Atoi(id)
global.Cron.Remove(cron.EntryID(idItem))
}
global.LOG.Infof("stop cronjob entryID: %s", cronjob.EntryIDs)
}
return cronjobRepo.Update(cronjob.ID, map[string]interface{}{"status": status, "entry_id": entryID})
return cronjobRepo.Update(cronjob.ID, map[string]interface{}{"status": status, "entry_ids": entryIDs})
}
func (u *CronjobService) AddCronJob(cronjob *model.Cronjob) (int, error) {
@ -328,26 +347,3 @@ func mkdirAndWriteFile(cronjob *model.Cronjob, startTime time.Time, msg []byte)
write.Flush()
return path, nil
}
func loadSpec(cronjob model.Cronjob) string {
switch cronjob.SpecType {
case "perMonth":
return fmt.Sprintf("%v %v %v * *", cronjob.Minute, cronjob.Hour, cronjob.Day)
case "perWeek":
return fmt.Sprintf("%v %v * * %v", cronjob.Minute, cronjob.Hour, cronjob.Week)
case "perNDay":
return fmt.Sprintf("%v %v */%v * *", cronjob.Minute, cronjob.Hour, cronjob.Day)
case "perDay":
return fmt.Sprintf("%v %v * * *", cronjob.Minute, cronjob.Hour)
case "perNHour":
return fmt.Sprintf("%v */%v * * *", cronjob.Minute, cronjob.Hour)
case "perHour":
return fmt.Sprintf("%v * * * *", cronjob.Minute)
case "perNMinute":
return fmt.Sprintf("@every %vm", cronjob.Minute)
case "perNSecond":
return fmt.Sprintf("@every %vs", cronjob.Second)
default:
return ""
}
}

View File

@ -66,11 +66,11 @@ func Run() {
global.LOG.Errorf("start my cronjob failed, err: %v", err)
}
for i := 0; i < len(cronJobs); i++ {
entryID, err := service.NewICronjobService().StartJob(&cronJobs[i])
entryIDs, err := service.NewICronjobService().StartJob(&cronJobs[i])
if err != nil {
global.LOG.Errorf("start %s job %s failed, err: %v", cronJobs[i].Type, cronJobs[i].Name, err)
}
if err := repo.NewICronjobRepo().Update(cronJobs[i].ID, map[string]interface{}{"entry_id": entryID}); err != nil {
if err := repo.NewICronjobRepo().Update(cronJobs[i].ID, map[string]interface{}{"entry_ids": entryIDs}); err != nil {
global.LOG.Errorf("update cronjob %s %s failed, err: %v", cronJobs[i].Type, cronJobs[i].Name, err)
}
}

View File

@ -67,6 +67,7 @@ func Init() {
migrations.AddPostgresqlSuperUser,
migrations.UpdateCronjobWithWebsite,
migrations.UpdateOneDriveToken,
migrations.UpdateCronjobSpec,
})
if err := m.Migrate(); err != nil {
global.LOG.Error(err)

View File

@ -254,3 +254,20 @@ var UpdateOneDriveToken = &gormigrate.Migration{
return nil
},
}
var UpdateCronjobSpec = &gormigrate.Migration{
ID: "20240122-update-cronjob-spec",
Migrate: func(tx *gorm.DB) error {
if err := tx.AutoMigrate(&model.Cronjob{}); err != nil {
return err
}
_ = tx.Exec("ALTER TABLE cronjobs DROP COLUMN spec_type;").Error
_ = tx.Exec("ALTER TABLE cronjobs DROP COLUMN week;").Error
_ = tx.Exec("ALTER TABLE cronjobs DROP COLUMN day;").Error
_ = tx.Exec("ALTER TABLE cronjobs DROP COLUMN hour;").Error
_ = tx.Exec("ALTER TABLE cronjobs DROP COLUMN minute;").Error
_ = tx.Exec("ALTER TABLE cronjobs DROP COLUMN second;").Error
return nil
},
}

View File

@ -14918,7 +14918,7 @@ const docTemplate = `{
"type": "object",
"required": [
"name",
"specType",
"spec",
"type"
],
"properties": {
@ -14928,9 +14928,6 @@ const docTemplate = `{
"containerName": {
"type": "string"
},
"day": {
"type": "integer"
},
"dbName": {
"type": "string"
},
@ -14940,15 +14937,9 @@ const docTemplate = `{
"exclusionRules": {
"type": "string"
},
"hour": {
"type": "integer"
},
"keepLocal": {
"type": "boolean"
},
"minute": {
"type": "integer"
},
"name": {
"type": "string"
},
@ -14959,13 +14950,10 @@ const docTemplate = `{
"script": {
"type": "string"
},
"second": {
"type": "integer"
},
"sourceDir": {
"type": "string"
},
"specType": {
"spec": {
"type": "string"
},
"targetDirID": {
@ -14979,11 +14967,6 @@ const docTemplate = `{
},
"website": {
"type": "string"
},
"week": {
"type": "integer",
"maximum": 6,
"minimum": 0
}
}
},
@ -15007,7 +14990,7 @@ const docTemplate = `{
"required": [
"id",
"name",
"specType"
"spec"
],
"properties": {
"appID": {
@ -15016,9 +14999,6 @@ const docTemplate = `{
"containerName": {
"type": "string"
},
"day": {
"type": "integer"
},
"dbName": {
"type": "string"
},
@ -15028,18 +15008,12 @@ const docTemplate = `{
"exclusionRules": {
"type": "string"
},
"hour": {
"type": "integer"
},
"id": {
"type": "integer"
},
"keepLocal": {
"type": "boolean"
},
"minute": {
"type": "integer"
},
"name": {
"type": "string"
},
@ -15050,13 +15024,10 @@ const docTemplate = `{
"script": {
"type": "string"
},
"second": {
"type": "integer"
},
"sourceDir": {
"type": "string"
},
"specType": {
"spec": {
"type": "string"
},
"targetDirID": {
@ -15067,11 +15038,6 @@ const docTemplate = `{
},
"website": {
"type": "string"
},
"week": {
"type": "integer",
"maximum": 6,
"minimum": 0
}
}
},

View File

@ -14911,7 +14911,7 @@
"type": "object",
"required": [
"name",
"specType",
"spec",
"type"
],
"properties": {
@ -14921,9 +14921,6 @@
"containerName": {
"type": "string"
},
"day": {
"type": "integer"
},
"dbName": {
"type": "string"
},
@ -14933,15 +14930,9 @@
"exclusionRules": {
"type": "string"
},
"hour": {
"type": "integer"
},
"keepLocal": {
"type": "boolean"
},
"minute": {
"type": "integer"
},
"name": {
"type": "string"
},
@ -14952,13 +14943,10 @@
"script": {
"type": "string"
},
"second": {
"type": "integer"
},
"sourceDir": {
"type": "string"
},
"specType": {
"spec": {
"type": "string"
},
"targetDirID": {
@ -14972,11 +14960,6 @@
},
"website": {
"type": "string"
},
"week": {
"type": "integer",
"maximum": 6,
"minimum": 0
}
}
},
@ -15000,7 +14983,7 @@
"required": [
"id",
"name",
"specType"
"spec"
],
"properties": {
"appID": {
@ -15009,9 +14992,6 @@
"containerName": {
"type": "string"
},
"day": {
"type": "integer"
},
"dbName": {
"type": "string"
},
@ -15021,18 +15001,12 @@
"exclusionRules": {
"type": "string"
},
"hour": {
"type": "integer"
},
"id": {
"type": "integer"
},
"keepLocal": {
"type": "boolean"
},
"minute": {
"type": "integer"
},
"name": {
"type": "string"
},
@ -15043,13 +15017,10 @@
"script": {
"type": "string"
},
"second": {
"type": "integer"
},
"sourceDir": {
"type": "string"
},
"specType": {
"spec": {
"type": "string"
},
"targetDirID": {
@ -15060,11 +15031,6 @@
},
"website": {
"type": "string"
},
"week": {
"type": "integer",
"maximum": 6,
"minimum": 0
}
}
},

View File

@ -582,20 +582,14 @@ definitions:
type: string
containerName:
type: string
day:
type: integer
dbName:
type: string
dbType:
type: string
exclusionRules:
type: string
hour:
type: integer
keepLocal:
type: boolean
minute:
type: integer
name:
type: string
retainCopies:
@ -603,11 +597,9 @@ definitions:
type: integer
script:
type: string
second:
type: integer
sourceDir:
type: string
specType:
spec:
type: string
targetDirID:
type: integer
@ -617,13 +609,9 @@ definitions:
type: string
website:
type: string
week:
maximum: 6
minimum: 0
type: integer
required:
- name
- specType
- spec
- type
type: object
dto.CronjobDownload:
@ -642,22 +630,16 @@ definitions:
type: string
containerName:
type: string
day:
type: integer
dbName:
type: string
dbType:
type: string
exclusionRules:
type: string
hour:
type: integer
id:
type: integer
keepLocal:
type: boolean
minute:
type: integer
name:
type: string
retainCopies:
@ -665,11 +647,9 @@ definitions:
type: integer
script:
type: string
second:
type: integer
sourceDir:
type: string
specType:
spec:
type: string
targetDirID:
type: integer
@ -677,14 +657,10 @@ definitions:
type: string
website:
type: string
week:
maximum: 6
minimum: 0
type: integer
required:
- id
- name
- specType
- spec
type: object
dto.CronjobUpdateStatus:
properties:

View File

@ -5,12 +5,8 @@ export namespace Cronjob {
id: number;
name: string;
type: string;
specType: string;
week: number;
day: number;
hour: number;
minute: number;
second: number;
spec: string;
specObjs: Array<SpecObj>;
script: string;
inContainer: boolean;
@ -31,12 +27,8 @@ export namespace Cronjob {
export interface CronjobCreate {
name: string;
type: string;
specType: string;
week: number;
day: number;
hour: number;
minute: number;
second: number;
spec: string;
specObjs: Array<SpecObj>;
script: string;
website: string;
@ -49,14 +41,17 @@ export namespace Cronjob {
targetDirID: number;
retainCopies: number;
}
export interface CronjobUpdate {
id: number;
export interface SpecObj {
specType: string;
week: number;
day: number;
hour: number;
minute: number;
second: number;
}
export interface CronjobUpdate {
id: number;
spec: string;
script: string;
website: string;

View File

@ -0,0 +1,227 @@
import { Cronjob } from '@/api/interface/cronjob';
import i18n from '@/lang';
import { loadZero } from '@/utils/util';
export const specOptions = [
{ label: i18n.global.t('cronjob.perMonth'), value: 'perMonth' },
{ label: i18n.global.t('cronjob.perWeek'), value: 'perWeek' },
{ label: i18n.global.t('cronjob.perDay'), value: 'perDay' },
{ label: i18n.global.t('cronjob.perHour'), value: 'perHour' },
{ label: i18n.global.t('cronjob.perNDay'), value: 'perNDay' },
{ label: i18n.global.t('cronjob.perNHour'), value: 'perNHour' },
{ label: i18n.global.t('cronjob.perNMinute'), value: 'perNMinute' },
{ label: i18n.global.t('cronjob.perNSecond'), value: 'perNSecond' },
];
export const weekOptions = [
{ label: i18n.global.t('cronjob.monday'), value: 1 },
{ label: i18n.global.t('cronjob.tuesday'), value: 2 },
{ label: i18n.global.t('cronjob.wednesday'), value: 3 },
{ label: i18n.global.t('cronjob.thursday'), value: 4 },
{ label: i18n.global.t('cronjob.friday'), value: 5 },
{ label: i18n.global.t('cronjob.saturday'), value: 6 },
{ label: i18n.global.t('cronjob.sunday'), value: 0 },
];
function loadWeek(i: number) {
for (const week of weekOptions) {
if (week.value === i) {
return week.label;
}
}
return '';
}
export function loadDefaultSpec(type: string) {
let item = {} as Cronjob.SpecObj;
switch (type) {
case 'shell':
item.specType = 'perWeek';
item.week = 1;
item.hour = 1;
item.minute = 30;
break;
case 'app':
item.specType = 'perDay';
item.hour = 2;
item.minute = 30;
break;
case 'database':
item.specType = 'perDay';
item.hour = 2;
item.minute = 30;
break;
case 'clean':
case 'website':
item.specType = 'perWeek';
item.week = 1;
item.hour = 1;
item.minute = 30;
break;
case 'log':
case 'snapshot':
item.specType = 'perWeek';
item.week = 1;
item.hour = 1;
item.minute = 30;
break;
case 'directory':
item.specType = 'perDay';
item.hour = 1;
item.minute = 30;
break;
case 'curl':
item.specType = 'perWeek';
item.week = 1;
item.hour = 1;
item.minute = 30;
break;
}
return item;
}
export function checkScript(specType: string, week, day, hour, minute, second) {
switch (specType) {
case 'perMonth':
return day > 0 && day < 32 && hour >= 0 && hour < 24 && minute >= 0 && minute < 60;
case 'perWeek':
return week >= 0 && week < 7 && hour >= 0 && hour < 24 && minute >= 0 && minute < 60;
case 'perDay':
return hour >= 0 && hour < 24 && minute >= 0 && minute < 60;
case 'perHour':
return minute >= 0 && minute < 60;
case 'perNDay':
return day > 0 && day < 366 && hour >= 0 && hour < 24 && minute >= 0 && minute < 60;
case 'perNHour':
return hour > 0 && hour < 8784 && minute >= 0 && minute < 60;
case 'perNMinute':
return minute > 0 && minute < 527040;
case 'perNSecond':
return second > 0 && second < 31622400;
}
}
export function transObjToSpec(specType: string, week, day, hour, minute, second): string {
switch (specType) {
case 'perMonth':
return `${minute} ${hour} ${day} * *`;
case 'perWeek':
return `${minute} ${hour} * * ${week}`;
case 'perNDay':
return `${minute} ${hour} */${day} * *`;
case 'perDay':
return `${minute} ${hour} * * *`;
case 'perNHour':
return `${minute} */${hour} * * *`;
case 'perHour':
return `${minute} * * * *`;
case 'perNMinute':
return `@every ${minute}m`;
case 'perNSecond':
return `@every ${second}s`;
default:
return '';
}
}
export function transSpecToObj(spec: string) {
let specs = spec.split(' ');
let specItem = {
specType: 'perNMinute',
week: 0,
day: 0,
hour: 0,
minute: 0,
second: 0,
};
if (specs.length === 2) {
if (specs[1].indexOf('m') !== -1) {
specItem.specType = 'perNMinute';
specItem.minute = Number(specs[1].replaceAll('m', ''));
return specItem;
} else {
specItem.specType = 'perNSecond';
specItem.second = Number(specs[1].replaceAll('s', ''));
return specItem;
}
}
if (specs.length !== 5 || specs[0] === '*') {
return null;
}
specItem.minute = Number(specs[0]);
if (specs[1] === '*') {
specItem.specType = 'perHour';
return specItem;
}
if (specs[1].indexOf('*/') !== -1) {
specItem.specType = 'perNHour';
specItem.hour = Number(specs[1].replaceAll('*/', ''));
return specItem;
}
specItem.hour = Number(specs[1]);
if (specs[2].indexOf('*/') !== -1) {
specItem.specType = 'perNDay';
specItem.day = Number(specs[2].replaceAll('*/', ''));
return specItem;
}
if (specs[2] !== '*') {
specItem.specType = 'perMonth';
specItem.day = Number(specs[2]);
return specItem;
}
if (specs[4] !== '*') {
specItem.specType = 'perWeek';
specItem.week = Number(specs[4]);
return specItem;
}
specItem.specType = 'perDay';
return specItem;
}
export function transSpecToStr(spec: string): string {
const specObj = transSpecToObj(spec);
let str = '';
if (specObj.specType.indexOf('N') === -1 || specObj.specType === 'perWeek') {
str += i18n.global.t('cronjob.' + specObj.specType) + ' ';
} else {
str += i18n.global.t('cronjob.per') + ' ';
}
switch (specObj.specType) {
case 'perMonth':
str +=
specObj.day +
i18n.global.t('cronjob.day') +
' ' +
loadZero(specObj.hour) +
':' +
loadZero(specObj.minute);
break;
case 'perWeek':
str += loadWeek(specObj.week) + ' ' + loadZero(specObj.hour) + ':' + loadZero(specObj.minute);
break;
case 'perDay':
str += loadZero(specObj.hour) + ':' + loadZero(specObj.minute);
break;
case 'perNDay':
str +=
specObj.day +
i18n.global.t('commons.units.day') +
', ' +
loadZero(specObj.hour) +
':' +
loadZero(specObj.minute);
break;
case 'perNHour':
str += specObj.hour + i18n.global.t('commons.units.hour') + ', ' + loadZero(specObj.minute);
break;
case 'perHour':
str += loadZero(specObj.minute);
break;
case 'perNMinute':
str += loadZero(specObj.minute) + i18n.global.t('commons.units.minute');
break;
case 'perNSecond':
str += loadZero(specObj.second) + i18n.global.t('commons.units.second');
break;
}
return str + ' ' + i18n.global.t('cronjob.handle');
}

View File

@ -81,35 +81,23 @@
</el-table-column>
<el-table-column :label="$t('cronjob.cronSpec')" show-overflow-tooltip :min-width="120">
<template #default="{ row }">
<span v-if="row.specType.indexOf('N') === -1 || row.specType === 'perWeek'">
{{ $t('cronjob.' + row.specType) }}&nbsp;
</span>
<span v-else>{{ $t('cronjob.per') }}</span>
<span v-if="row.specType === 'perMonth'">
{{ row.day }}{{ $t('cronjob.day') }} {{ loadZero(row.hour) }} :
{{ loadZero(row.minute) }}
</span>
<span v-if="row.specType === 'perWeek'">
{{ loadWeek(row.week) }} {{ loadZero(row.hour) }} : {{ loadZero(row.minute) }}
</span>
<span v-if="row.specType === 'perDay'">
&#32;{{ loadZero(row.hour) }} : {{ loadZero(row.minute) }}
</span>
<span v-if="row.specType === 'perNDay'">
{{ row.day }} {{ $t('commons.units.day') }}, {{ loadZero(row.hour) }} :
{{ loadZero(row.minute) }}
</span>
<span v-if="row.specType === 'perNHour'">
{{ row.hour }}{{ $t('commons.units.hour') }}, {{ loadZero(row.minute) }}
</span>
<span v-if="row.specType === 'perHour'">{{ loadZero(row.minute) }}</span>
<span v-if="row.specType === 'perNMinute'">
{{ row.minute }}{{ $t('commons.units.minute') }}
</span>
<span v-if="row.specType === 'perNSecond'">
{{ row.second }}{{ $t('commons.units.second') }}
</span>
{{ $t('cronjob.handle') }}
<div v-for="(item, index) of row.spec.split(',')" :key="index" class="mt-1">
<div v-if="row.expand || (!row.expand && index < 3)">
<el-tag>
{{ transSpecToStr(item) }}
</el-tag>
</div>
</div>
<div v-if="!row.expand && row.spec.split(',').length > 3">
<el-button type="primary" link @click="row.expand = true">
{{ $t('commons.button.expand') }}...
</el-button>
</div>
<div v-if="row.expand && row.spec.split(',').length > 3">
<el-button type="primary" link @click="row.expand = false">
{{ $t('commons.button.collapse') }}
</el-button>
</div>
</template>
</el-table-column>
<el-table-column :label="$t('cronjob.retainCopies')" :min-width="90" prop="retainCopies" />
@ -158,13 +146,13 @@ import TableSetting from '@/components/table-setting/index.vue';
import Tooltip from '@/components/tooltip/index.vue';
import OperateDialog from '@/views/cronjob/operate/index.vue';
import Records from '@/views/cronjob/record/index.vue';
import { loadZero } from '@/utils/util';
import { onMounted, reactive, ref } from 'vue';
import { deleteCronjob, getCronjobPage, handleOnce, updateStatus } from '@/api/modules/cronjob';
import i18n from '@/lang';
import { Cronjob } from '@/api/interface/cronjob';
import { ElMessageBox } from 'element-plus';
import { MsgSuccess } from '@/utils/message';
import { transSpecToStr } from './helper';
const loading = ref();
const selects = ref<any>([]);
@ -186,16 +174,6 @@ const paginationConfig = reactive({
});
const searchName = ref();
const weekOptions = [
{ label: i18n.global.t('cronjob.monday'), value: 1 },
{ label: i18n.global.t('cronjob.tuesday'), value: 2 },
{ label: i18n.global.t('cronjob.wednesday'), value: 3 },
{ label: i18n.global.t('cronjob.thursday'), value: 4 },
{ label: i18n.global.t('cronjob.friday'), value: 5 },
{ label: i18n.global.t('cronjob.saturday'), value: 6 },
{ label: i18n.global.t('cronjob.sunday'), value: 0 },
];
const search = async (column?: any) => {
paginationConfig.orderBy = column?.order ? column.prop : paginationConfig.orderBy;
paginationConfig.order = column?.order ? column.order : paginationConfig.order;
@ -229,13 +207,17 @@ const dialogRef = ref();
const onOpenDialog = async (
title: string,
rowData: Partial<Cronjob.CronjobInfo> = {
specType: 'perMonth',
specObjs: [
{
specType: 'perMonth',
week: 1,
day: 3,
hour: 1,
minute: 30,
second: 30,
},
],
type: 'shell',
week: 1,
day: 3,
hour: 1,
minute: 30,
second: 30,
keepLocal: true,
retainCopies: 7,
},
@ -370,14 +352,7 @@ const buttons = [
},
},
];
function loadWeek(i: number) {
for (const week of weekOptions) {
if (week.value === i) {
return week.label;
}
}
return '';
}
onMounted(() => {
search();
});

View File

@ -64,46 +64,64 @@
</el-form-item>
<el-form-item :label="$t('cronjob.cronSpec')" prop="spec">
<el-select class="specTypeClass" v-model="dialogData.rowData!.specType">
<el-option
v-for="item in specOptions"
:key="item.label"
:value="item.value"
:label="item.label"
/>
</el-select>
<el-select
v-if="dialogData.rowData!.specType === 'perWeek'"
class="specClass"
v-model="dialogData.rowData!.week"
>
<el-option
v-for="item in weekOptions"
:key="item.label"
:value="item.value"
:label="item.label"
/>
</el-select>
<el-input v-if="hasDay()" class="specClass" v-model.number="dialogData.rowData!.day">
<template #append>{{ $t('cronjob.day') }}</template>
</el-input>
<el-input v-if="hasHour()" class="specClass" v-model.number="dialogData.rowData!.hour">
<template #append>{{ $t('commons.units.hour') }}</template>
</el-input>
<el-input
v-if="dialogData.rowData!.specType !== 'perNSecond'"
class="specClass"
v-model.number="dialogData.rowData!.minute"
>
<template #append>{{ $t('commons.units.minute') }}</template>
</el-input>
<el-input
v-if="dialogData.rowData!.specType === 'perNSecond'"
class="specClass"
v-model.number="dialogData.rowData!.second"
>
<template #append>{{ $t('commons.units.second') }}</template>
</el-input>
<div v-for="(specObj, index) of dialogData.rowData.specObjs" :key="index" style="width: 100%">
<el-select class="specTypeClass" v-model="specObj.specType">
<el-option
v-for="item in specOptions"
:key="item.label"
:value="item.value"
:label="item.label"
/>
</el-select>
<el-select v-if="specObj.specType === 'perWeek'" class="specClass" v-model="specObj.week">
<el-option
v-for="item in weekOptions"
:key="item.label"
:value="item.value"
:label="item.label"
/>
</el-select>
<el-input v-if="hasDay(specObj)" class="specClass" v-model.number="specObj.day">
<template #append>
<div class="append">{{ $t('cronjob.day') }}</div>
</template>
</el-input>
<el-input v-if="hasHour(specObj)" class="specClass" v-model.number="specObj.hour">
<template #append>
<div class="append">{{ $t('commons.units.hour') }}</div>
</template>
</el-input>
<el-input
v-if="specObj.specType !== 'perNSecond'"
class="specClass"
v-model.number="specObj.minute"
>
<template #append>
<div class="append">{{ $t('commons.units.minute') }}</div>
</template>
</el-input>
<el-input
v-if="specObj.specType === 'perNSecond'"
class="specClass"
v-model.number="specObj.second"
>
<template #append>
<div class="append">{{ $t('commons.units.second') }}</div>
</template>
</el-input>
<el-button
link
type="primary"
style="float: right; margin-top: 5px"
@click="handleSpecDelete(index)"
>
{{ $t('commons.button.delete') }}
</el-button>
<el-divider class="divider" />
</div>
<el-button class="mt-3" @click="handleSpecAdd()">
{{ $t('commons.button.add') }}
</el-button>
</el-form-item>
<el-form-item v-if="hasScript()">
@ -296,7 +314,7 @@
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import { checkNumberRange, Rules } from '@/global/form-rules';
import { Rules } from '@/global/form-rules';
import FileList from '@/components/file-list/index.vue';
import { getBackupList } from '@/api/modules/setting';
import i18n from '@/lang';
@ -311,6 +329,7 @@ import { useRouter } from 'vue-router';
import { listContainer } from '@/api/modules/container';
import { Database } from '@/api/interface/database';
import { ListAppInstalled } from '@/api/modules/app';
import { checkScript, loadDefaultSpec, specOptions, transObjToSpec, transSpecToObj, weekOptions } from './../helper';
const router = useRouter();
interface DialogProps {
@ -326,6 +345,13 @@ const dialogData = ref<DialogProps>({
const acceptParams = (params: DialogProps): void => {
dialogData.value = params;
if (dialogData.value.rowData?.spec) {
let objs = [];
for (const item of dialogData.value.rowData.spec.split(',')) {
objs.push(transSpecToObj(item));
}
dialogData.value.rowData.specObjs = objs;
}
if (dialogData.value.title === 'create') {
changeType();
dialogData.value.rowData.dbType = 'mysql';
@ -373,96 +399,52 @@ const dbInfo = reactive({
});
const verifySpec = (rule: any, value: any, callback: any) => {
switch (dialogData.value.rowData!.specType) {
case 'perMonth':
case 'perNDay':
if (
!(
Number.isInteger(dialogData.value.rowData!.day) &&
Number.isInteger(dialogData.value.rowData!.hour) &&
Number.isInteger(dialogData.value.rowData!.minute)
)
) {
callback(new Error(i18n.global.t('cronjob.cronSpecRule')));
}
break;
case 'perWeek':
if (
!(
Number.isInteger(dialogData.value.rowData!.week) &&
Number.isInteger(dialogData.value.rowData!.hour) &&
Number.isInteger(dialogData.value.rowData!.minute)
)
) {
callback(new Error(i18n.global.t('cronjob.cronSpecRule')));
}
break;
case 'perDay':
if (
!(
Number.isInteger(dialogData.value.rowData!.hour) &&
Number.isInteger(dialogData.value.rowData!.minute)
)
) {
callback(new Error(i18n.global.t('cronjob.cronSpecRule')));
}
break;
case 'perNHour':
if (
!(
Number.isInteger(dialogData.value.rowData!.hour) &&
Number.isInteger(dialogData.value.rowData!.minute)
)
) {
callback(new Error(i18n.global.t('cronjob.cronSpecRule')));
}
break;
case 'perHour':
case 'perNMinute':
if (!Number.isInteger(dialogData.value.rowData!.minute)) {
callback(new Error(i18n.global.t('cronjob.cronSpecRule')));
}
break;
case 'perNSecond':
if (!Number.isInteger(dialogData.value.rowData!.second)) {
callback(new Error(i18n.global.t('cronjob.cronSpecRule')));
}
break;
for (const item of dialogData.value.rowData!.specObjs) {
switch (item.specType) {
case 'perMonth':
case 'perNDay':
if (!(Number.isInteger(item.day) && Number.isInteger(item.hour) && Number.isInteger(item.minute))) {
callback(new Error(i18n.global.t('cronjob.cronSpecRule')));
}
break;
case 'perWeek':
if (!(Number.isInteger(item.week) && Number.isInteger(item.hour) && Number.isInteger(item.minute))) {
callback(new Error(i18n.global.t('cronjob.cronSpecRule')));
}
break;
case 'perDay':
if (!(Number.isInteger(item.hour) && Number.isInteger(item.minute))) {
callback(new Error(i18n.global.t('cronjob.cronSpecRule')));
}
break;
case 'perNHour':
if (!(Number.isInteger(item.hour) && Number.isInteger(item.minute))) {
callback(new Error(i18n.global.t('cronjob.cronSpecRule')));
}
break;
case 'perHour':
case 'perNMinute':
if (!Number.isInteger(item.minute)) {
callback(new Error(i18n.global.t('cronjob.cronSpecRule')));
}
break;
case 'perNSecond':
if (!Number.isInteger(item.second)) {
callback(new Error(i18n.global.t('cronjob.cronSpecRule')));
}
break;
}
}
callback();
};
const specOptions = [
{ label: i18n.global.t('cronjob.perMonth'), value: 'perMonth' },
{ label: i18n.global.t('cronjob.perWeek'), value: 'perWeek' },
{ label: i18n.global.t('cronjob.perDay'), value: 'perDay' },
{ label: i18n.global.t('cronjob.perHour'), value: 'perHour' },
{ label: i18n.global.t('cronjob.perNDay'), value: 'perNDay' },
{ label: i18n.global.t('cronjob.perNHour'), value: 'perNHour' },
{ label: i18n.global.t('cronjob.perNMinute'), value: 'perNMinute' },
{ label: i18n.global.t('cronjob.perNSecond'), value: 'perNSecond' },
];
const weekOptions = [
{ label: i18n.global.t('cronjob.monday'), value: 1 },
{ label: i18n.global.t('cronjob.tuesday'), value: 2 },
{ label: i18n.global.t('cronjob.wednesday'), value: 3 },
{ label: i18n.global.t('cronjob.thursday'), value: 4 },
{ label: i18n.global.t('cronjob.friday'), value: 5 },
{ label: i18n.global.t('cronjob.saturday'), value: 6 },
{ label: i18n.global.t('cronjob.sunday'), value: 0 },
];
const rules = reactive({
name: [Rules.requiredInput],
type: [Rules.requiredSelect],
specType: [Rules.requiredSelect],
spec: [
{ validator: verifySpec, trigger: 'blur', required: true },
{ validator: verifySpec, trigger: 'change', required: true },
],
week: [Rules.requiredSelect, Rules.number],
day: [Rules.number, checkNumberRange(1, 31)],
hour: [Rules.number, checkNumberRange(1, 23)],
minute: [Rules.number, checkNumberRange(1, 59)],
script: [Rules.requiredInput],
website: [Rules.requiredSelect],
@ -480,15 +462,11 @@ const loadDir = async (path: string) => {
dialogData.value.rowData!.sourceDir = path;
};
const hasDay = () => {
return dialogData.value.rowData!.specType === 'perMonth' || dialogData.value.rowData!.specType === 'perNDay';
const hasDay = (item: any) => {
return item.specType === 'perMonth' || item.specType === 'perNDay';
};
const hasHour = () => {
return (
dialogData.value.rowData!.specType !== 'perHour' &&
dialogData.value.rowData!.specType !== 'perNMinute' &&
dialogData.value.rowData!.specType !== 'perNSecond'
);
const hasHour = (item: any) => {
return item.specType !== 'perHour' && item.specType !== 'perNMinute' && item.specType !== 'perNSecond';
};
const loadDatabases = async (dbType: string) => {
@ -497,57 +475,34 @@ const loadDatabases = async (dbType: string) => {
};
const changeType = () => {
switch (dialogData.value.rowData!.type) {
case 'shell':
dialogData.value.rowData.specType = 'perWeek';
dialogData.value.rowData.week = 1;
dialogData.value.rowData.hour = 1;
dialogData.value.rowData.minute = 30;
break;
case 'app':
dialogData.value.rowData.specType = 'perDay';
dialogData.value.rowData.hour = 2;
dialogData.value.rowData.minute = 30;
break;
case 'database':
dialogData.value.rowData.specType = 'perDay';
dialogData.value.rowData.hour = 2;
dialogData.value.rowData.minute = 30;
break;
case 'clean':
case 'website':
dialogData.value.rowData.specType = 'perWeek';
dialogData.value.rowData.week = 1;
dialogData.value.rowData.hour = 1;
dialogData.value.rowData.minute = 30;
break;
case 'log':
case 'snapshot':
dialogData.value.rowData.specType = 'perWeek';
dialogData.value.rowData.week = 1;
dialogData.value.rowData.hour = 1;
dialogData.value.rowData.minute = 30;
dialogData.value.rowData.keepLocal = false;
dialogData.value.rowData.targetDirID = null;
for (const item of backupOptions.value) {
if (item.label !== i18n.global.t('setting.LOCAL')) {
dialogData.value.rowData.targetDirID = item.value;
break;
}
if (dialogData.value.rowData.type === 'snapshot') {
dialogData.value.rowData.keepLocal = false;
dialogData.value.rowData.targetDirID = null;
for (const item of backupOptions.value) {
if (item.label !== i18n.global.t('setting.LOCAL')) {
dialogData.value.rowData.targetDirID = item.value;
break;
}
break;
case 'directory':
dialogData.value.rowData.specType = 'perDay';
dialogData.value.rowData.hour = 1;
dialogData.value.rowData.minute = 30;
break;
case 'curl':
dialogData.value.rowData.specType = 'perWeek';
dialogData.value.rowData.week = 1;
dialogData.value.rowData.hour = 1;
dialogData.value.rowData.minute = 30;
break;
}
}
dialogData.value.rowData!.specObjs = [loadDefaultSpec(dialogData.value.rowData.type)];
};
const handleSpecAdd = () => {
let item = {
specType: 'perWeek',
week: 1,
day: 0,
hour: 1,
minute: 30,
second: 0,
};
dialogData.value.rowData!.specObjs.push(item);
};
const handleSpecDelete = (index: number) => {
dialogData.value.rowData!.specObjs.splice(index, 1);
};
const loadBackups = async () => {
@ -597,40 +552,21 @@ function hasScript() {
return dialogData.value.rowData!.type === 'shell';
}
function checkScript() {
let row = dialogData.value.rowData;
switch (row.specType) {
case 'perMonth':
return row.day > 0 && row.day < 32 && row.hour >= 0 && row.hour < 24 && row.minute >= 0 && row.minute < 60;
case 'perWeek':
return (
row.week >= 0 && row.week < 7 && row.hour >= 0 && row.hour < 24 && row.minute >= 0 && row.minute < 60
);
case 'perDay':
return row.hour >= 0 && row.hour < 24 && row.minute >= 0 && row.minute < 60;
case 'perHour':
return row.minute >= 0 && row.minute < 60;
case 'perNDay':
return row.day > 0 && row.day < 366 && row.hour >= 0 && row.hour < 24 && row.minute >= 0 && row.minute < 60;
case 'perNHour':
return row.hour > 0 && row.hour < 8784 && row.minute >= 0 && row.minute < 60;
case 'perNMinute':
return row.minute > 0 && row.minute < 527040;
case 'perNSecond':
return row.second > 0 && row.second < 31622400;
}
}
const onSubmit = async (formEl: FormInstance | undefined) => {
dialogData.value.rowData.week = Number(dialogData.value.rowData.week);
dialogData.value.rowData.day = Number(dialogData.value.rowData.day);
dialogData.value.rowData.hour = Number(dialogData.value.rowData.hour);
dialogData.value.rowData.minute = Number(dialogData.value.rowData.minute);
dialogData.value.rowData.second = Number(dialogData.value.rowData.second);
if (!checkScript()) {
MsgError(i18n.global.t('cronjob.cronSpecHelper'));
return;
const specs = [];
for (const item of dialogData.value.rowData.specObjs) {
if (!checkScript(item.specType, item.week, item.day, item.hour, item.minute, item.second)) {
MsgError(i18n.global.t('cronjob.cronSpecHelper'));
return;
}
const itemSpec = transObjToSpec(item.specType, item.week, item.day, item.hour, item.minute, item.second);
if (itemSpec === '') {
MsgError(i18n.global.t('cronjob.cronSpecHelper'));
return;
}
specs.push(itemSpec);
}
dialogData.value.rowData.spec = specs.join(',');
if (!formEl) return;
formEl.validate(async (valid) => {
if (!valid) return;
@ -660,14 +596,20 @@ defineExpose({
</script>
<style scoped lang="scss">
.specClass {
width: 22% !important;
width: 20% !important;
margin-left: 20px;
.append {
width: 20px;
}
}
@media only screen and (max-width: 1000px) {
.specClass {
width: 100% !important;
margin-top: 20px;
margin-left: 0;
.append {
width: 43px;
}
}
}
.specTypeClass {
@ -695,4 +637,12 @@ defineExpose({
margin-top: -3px;
}
}
.divider {
display: block;
height: 1px;
width: 100%;
margin: 3px 0;
border-top: 1px var(--el-border-color) var(--el-border-style);
}
</style>