定时任务支持批量启用禁用删除运行

This commit is contained in:
whyour 2021-05-08 14:28:05 +08:00
parent 2092e93dc2
commit 5d43add6eb
5 changed files with 211 additions and 97 deletions

View File

@ -51,18 +51,16 @@ export default (app: Router) => {
}, },
); );
route.get( route.put(
'/crons/:id/run', '/crons/run',
celebrate({ celebrate({
params: Joi.object({ body: Joi.array().items(Joi.string().required()),
id: Joi.string().required(),
}),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
const cronService = Container.get(CronService); const cronService = Container.get(CronService);
const data = await cronService.run(req.params.id); const data = await cronService.run(req.body);
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e); logger.error('🔥 error: %o', e);
@ -71,18 +69,16 @@ export default (app: Router) => {
}, },
); );
route.get( route.put(
'/crons/:id/disable', '/crons/disable',
celebrate({ celebrate({
params: Joi.object({ body: Joi.array().items(Joi.string().required()),
id: Joi.string().required(),
}),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
const cronService = Container.get(CronService); const cronService = Container.get(CronService);
const data = await cronService.disabled(req.params.id); const data = await cronService.disabled(req.body);
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e); logger.error('🔥 error: %o', e);
@ -91,18 +87,16 @@ export default (app: Router) => {
}, },
); );
route.get( route.put(
'/crons/:id/enable', '/crons/enable',
celebrate({ celebrate({
params: Joi.object({ body: Joi.array().items(Joi.string().required()),
id: Joi.string().required(),
}),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
const cronService = Container.get(CronService); const cronService = Container.get(CronService);
const data = await cronService.enabled(req.params.id); const data = await cronService.enabled(req.body);
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e); logger.error('🔥 error: %o', e);
@ -159,17 +153,15 @@ export default (app: Router) => {
); );
route.delete( route.delete(
'/crons/:id', '/crons',
celebrate({ celebrate({
params: Joi.object({ body: Joi.array().items(Joi.string().required()),
id: Joi.string().required(),
}),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
const cronService = Container.get(CronService); const cronService = Container.get(CronService);
const data = await cronService.remove(req.params.id); const data = await cronService.remove(req.body);
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e); logger.error('🔥 error: %o', e);

View File

@ -33,7 +33,7 @@ const run = async () => {
) { ) {
schedule.scheduleJob(task.schedule, function () { schedule.scheduleJob(task.schedule, function () {
let command = task.command as string; let command = task.command as string;
if (!command.startsWith('task ') && !command.startsWith('ql ')) { if (!command.includes('task ') && !command.includes('ql ')) {
command = `task ${command}`; command = `task ${command}`;
} }
exec(command); exec(command);

View File

@ -74,8 +74,8 @@ export default class CronService {
this.cronDb.update({ _id }, { $set: { stopped, saved: false } }); this.cronDb.update({ _id }, { $set: { stopped, saved: false } });
} }
public async remove(_id: string) { public async remove(ids: string[]) {
this.cronDb.remove({ _id }, {}); this.cronDb.remove({ _id: { $in: ids } }, { multi: true });
await this.set_crontab(); await this.set_crontab();
} }
@ -112,69 +112,84 @@ export default class CronService {
}); });
} }
public async run(_id: string) { public async run(ids: string[]) {
this.cronDb.find({ _id }).exec((err, docs: Crontab[]) => { this.cronDb.find({ _id: { $in: ids } }).exec((err, docs: Crontab[]) => {
let res = docs[0]; for (let i = 0; i < docs.length; i++) {
const doc = docs[i];
this.logger.silly('Running job'); this.runSingle(doc);
this.logger.silly('ID: ' + _id);
this.logger.silly('Original command: ' + res.command);
let logFile = `${config.manualLogPath}${res._id}.log`;
fs.writeFileSync(logFile, `开始执行...\n\n${new Date().toString()}\n`);
let cmdStr = res.command;
if (!cmdStr.startsWith('task') && !cmdStr.startsWith('ql ')) {
cmdStr = `task ${cmdStr}`;
} }
if (cmdStr.endsWith('.js')) {
cmdStr = `${cmdStr} now`;
}
const cmd = spawn(cmdStr, { shell: true });
this.cronDb.update({ _id }, { $set: { status: CrontabStatus.running } });
cmd.stdout.on('data', (data) => {
this.logger.silly(`stdout: ${data}`);
fs.appendFileSync(logFile, data);
});
cmd.stderr.on('data', (data) => {
this.logger.error(`stderr: ${data}`);
fs.appendFileSync(logFile, data);
});
cmd.on('close', (code) => {
this.logger.silly(`child process exited with code ${code}`);
this.cronDb.update({ _id }, { $set: { status: CrontabStatus.idle } });
});
cmd.on('error', (err) => {
this.logger.silly(err);
fs.appendFileSync(logFile, err.stack);
});
cmd.on('exit', (code: number, signal: any) => {
this.logger.silly(`cmd exit ${code}`);
this.cronDb.update({ _id }, { $set: { status: CrontabStatus.idle } });
fs.appendFileSync(logFile, `\n\n执行结束...`);
});
cmd.on('disconnect', () => {
this.logger.silly(`cmd disconnect`);
this.cronDb.update({ _id }, { $set: { status: CrontabStatus.idle } });
fs.appendFileSync(logFile, `\n\n连接断开...`);
});
}); });
} }
public async disabled(_id: string) { private async runSingle(cron: Crontab) {
this.cronDb.update({ _id }, { $set: { status: CrontabStatus.disabled } }); let { _id, command } = cron;
this.logger.silly('Running job');
this.logger.silly('ID: ' + _id);
this.logger.silly('Original command: ' + command);
let logFile = `${config.manualLogPath}${_id}.log`;
fs.writeFileSync(logFile, `开始执行...\n\n${new Date().toString()}\n`);
let cmdStr = command;
if (!cmdStr.includes('task ') && !cmdStr.includes('ql ')) {
cmdStr = `task ${cmdStr}`;
}
if (cmdStr.endsWith('.js')) {
cmdStr = `${cmdStr} now`;
}
const cmd = spawn(cmdStr, { shell: true });
this.cronDb.update({ _id }, { $set: { status: CrontabStatus.running } });
cmd.stdout.on('data', (data) => {
this.logger.silly(`stdout: ${data}`);
fs.appendFileSync(logFile, data);
});
cmd.stderr.on('data', (data) => {
this.logger.error(`stderr: ${data}`);
fs.appendFileSync(logFile, data);
});
cmd.on('close', (code) => {
this.logger.silly(`child process exited with code ${code}`);
this.cronDb.update({ _id }, { $set: { status: CrontabStatus.idle } });
});
cmd.on('error', (err) => {
this.logger.silly(err);
fs.appendFileSync(logFile, err.stack);
});
cmd.on('exit', (code: number, signal: any) => {
this.logger.silly(`cmd exit ${code}`);
this.cronDb.update({ _id }, { $set: { status: CrontabStatus.idle } });
fs.appendFileSync(logFile, `\n\n执行结束...`);
});
cmd.on('disconnect', () => {
this.logger.silly(`cmd disconnect`);
this.cronDb.update({ _id }, { $set: { status: CrontabStatus.idle } });
fs.appendFileSync(logFile, `\n\n连接断开...`);
});
}
public async disabled(ids: string[]) {
this.cronDb.update(
{ _id: { $in: ids } },
{ $set: { status: CrontabStatus.disabled } },
{ multi: true },
);
await this.set_crontab(); await this.set_crontab();
} }
public async enabled(_id: string) { public async enabled(ids: string[]) {
this.cronDb.update({ _id }, { $set: { status: CrontabStatus.idle } }); this.cronDb.update(
{ _id: { $in: ids } },
{ $set: { status: CrontabStatus.idle } },
{ multi: true },
);
} }
public async log(_id: string) { public async log(_id: string) {

View File

@ -434,7 +434,6 @@ const Config = () => {
<PageContainer <PageContainer
className="cookie-wrapper" className="cookie-wrapper"
title="Cookie管理" title="Cookie管理"
loading={loading}
extra={[ extra={[
<Button key="2" type="primary" onClick={() => addCookie()}> <Button key="2" type="primary" onClick={() => addCookie()}>
Cookie Cookie
@ -463,9 +462,9 @@ const Config = () => {
dataSource={value} dataSource={value}
rowKey="value" rowKey="value"
size="middle" size="middle"
bordered
scroll={{ x: 768 }} scroll={{ x: 768 }}
components={components} components={components}
loading={loading}
onRow={(record, index) => { onRow={(record, index) => {
return { return {
index, index,

View File

@ -39,6 +39,18 @@ enum CrontabStatus {
'disabled', 'disabled',
} }
enum OperationName {
'启用',
'禁用',
'运行',
}
enum OperationPath {
'enable',
'disable',
'run',
}
const Crontab = () => { const Crontab = () => {
const columns = [ const columns = [
{ {
@ -142,6 +154,7 @@ const Crontab = () => {
const [searchText, setSearchText] = useState(''); const [searchText, setSearchText] = useState('');
const [isLogModalVisible, setIsLogModalVisible] = useState(false); const [isLogModalVisible, setIsLogModalVisible] = useState(false);
const [logCron, setLogCron] = useState<any>(); const [logCron, setLogCron] = useState<any>();
const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]);
const getCrons = () => { const getCrons = () => {
setLoading(true); setLoading(true);
@ -177,7 +190,7 @@ const Crontab = () => {
), ),
onOk() { onOk() {
request request
.delete(`${config.apiPrefix}crons/${record._id}`) .delete(`${config.apiPrefix}crons`, { data: [record._id] })
.then((data: any) => { .then((data: any) => {
if (data.code === 200) { if (data.code === 200) {
notification.success({ notification.success({
@ -213,7 +226,7 @@ const Crontab = () => {
), ),
onOk() { onOk() {
request request
.get(`${config.apiPrefix}crons/${record._id}/run`) .put(`${config.apiPrefix}crons/run`, { data: [record._id] })
.then((data: any) => { .then((data: any) => {
if (data.code === 200) { if (data.code === 200) {
const result = [...value]; const result = [...value];
@ -252,21 +265,16 @@ const Crontab = () => {
), ),
onOk() { onOk() {
request request
.get( .put(
`${config.apiPrefix}crons/${record._id}/${ `${config.apiPrefix}crons/${
record.status === CrontabStatus.disabled ? 'enable' : 'disable' record.status === CrontabStatus.disabled ? 'enable' : 'disable'
}`, }`,
{ {
data: { _id: record._id }, data: [record._id],
}, },
) )
.then((data: any) => { .then((data: any) => {
if (data.code === 200) { if (data.code === 200) {
notification.success({
message: `${
record.status === CrontabStatus.disabled ? '启用' : '禁用'
}`,
});
const newStatus = const newStatus =
record.status === CrontabStatus.disabled record.status === CrontabStatus.disabled
? CrontabStatus.idle ? CrontabStatus.idle
@ -296,7 +304,7 @@ const Crontab = () => {
}> = ({ record, index }) => ( }> = ({ record, index }) => (
<Dropdown <Dropdown
arrow arrow
trigger={['click', 'hover']} trigger={['click']}
overlay={ overlay={
<Menu onClick={({ key }) => action(key, record, index)}> <Menu onClick={({ key }) => action(key, record, index)}>
<Menu.Item key="edit" icon={<EditOutlined />}> <Menu.Item key="edit" icon={<EditOutlined />}>
@ -383,6 +391,74 @@ const Crontab = () => {
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}; };
const onSelectChange = (selectedIds: any[]) => {
setSelectedRowIds(selectedIds);
};
const rowSelection = {
selectedRowIds,
onChange: onSelectChange,
selections: [
Table.SELECTION_ALL,
Table.SELECTION_INVERT,
Table.SELECTION_NONE,
],
};
const delCrons = () => {
Modal.confirm({
title: '确认删除',
content: <></>,
onOk() {
request
.delete(`${config.apiPrefix}crons`, { data: selectedRowIds })
.then((data: any) => {
if (data.code === 200) {
notification.success({
message: '批量删除成功',
});
getCrons();
} else {
notification.error({
message: data,
});
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
const operateCrons = (operationStatus: number) => {
Modal.confirm({
title: `确认${OperationName[operationStatus]}`,
content: <>{OperationName[operationStatus]}</>,
onOk() {
request
.put(`${config.apiPrefix}crons/${OperationPath[operationStatus]}`, {
data: selectedRowIds,
})
.then((data: any) => {
if (data.code === 200) {
notification.success({
message: `批量${OperationName[operationStatus]}成功`,
});
getCrons();
} else {
notification.error({
message: data,
});
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
useEffect(() => { useEffect(() => {
if (logCron) { if (logCron) {
localStorage.setItem('logCron', logCron._id); localStorage.setItem('logCron', logCron._id);
@ -410,7 +486,6 @@ const Crontab = () => {
<PageContainer <PageContainer
className="code-mirror-wrapper" className="code-mirror-wrapper"
title="定时任务" title="定时任务"
loading={loading}
extra={[ extra={[
<Search <Search
placeholder="请输入名称或者关键词" placeholder="请输入名称或者关键词"
@ -439,6 +514,38 @@ const Crontab = () => {
height: '100vh', height: '100vh',
}} }}
> >
{selectedRowIds.length > 0 && (
<div style={{ marginBottom: 16 }}>
<Button type="primary" onClick={delCrons}>
</Button>
<Button
type="primary"
onClick={() => operateCrons(0)}
style={{ marginLeft: 8 }}
>
</Button>
<Button
type="primary"
onClick={() => operateCrons(1)}
style={{ marginLeft: 8 }}
>
</Button>
<Button
type="primary"
onClick={() => operateCrons(2)}
style={{ marginLeft: 8 }}
>
</Button>
<span style={{ marginLeft: 8 }}>
<a>{selectedRowIds?.length}</a>
</span>
</div>
)}
<Table <Table
columns={columns} columns={columns}
pagination={{ pagination={{
@ -449,8 +556,9 @@ const Crontab = () => {
dataSource={value} dataSource={value}
rowKey="_id" rowKey="_id"
size="middle" size="middle"
bordered
scroll={{ x: 768 }} scroll={{ x: 768 }}
loading={loading}
rowSelection={rowSelection}
/> />
<CronLogModal <CronLogModal
visible={isLogModalVisible} visible={isLogModalVisible}