@ruiapp/rapid-core 0.8.10 → 0.8.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +361 -65
- package/dist/plugins/cronJob/CronJobPluginTypes.d.ts +26 -0
- package/dist/plugins/cronJob/entityWatchers/cronJobEntityWatchers.d.ts +7 -0
- package/dist/plugins/cronJob/entityWatchers/index.d.ts +6 -0
- package/dist/plugins/cronJob/models/CronJob.d.ts +3 -0
- package/dist/plugins/cronJob/models/index.d.ts +2 -0
- package/dist/plugins/cronJob/services/CronJobService.d.ts +6 -4
- package/dist/utilities/timeUtility.d.ts +1 -0
- package/package.json +1 -1
- package/src/plugins/cronJob/CronJobPlugin.ts +30 -5
- package/src/plugins/cronJob/CronJobPluginTypes.ts +29 -0
- package/src/plugins/cronJob/actionHandlers/runCronJob.ts +7 -14
- package/src/plugins/cronJob/entityWatchers/cronJobEntityWatchers.ts +24 -0
- package/src/plugins/cronJob/entityWatchers/index.ts +4 -0
- package/src/plugins/cronJob/models/CronJob.ts +129 -0
- package/src/plugins/cronJob/models/index.ts +3 -0
- package/src/plugins/cronJob/services/CronJobService.ts +181 -37
- package/src/utilities/timeUtility.ts +4 -0
package/dist/index.js
CHANGED
|
@@ -2498,6 +2498,9 @@ function getNowStringWithTimezone() {
|
|
|
2498
2498
|
}
|
|
2499
2499
|
function getDateString(timeString) {
|
|
2500
2500
|
return dayjs__default["default"](timeString).format("YYYY-MM-DD");
|
|
2501
|
+
}
|
|
2502
|
+
function formatDateTimeWithTimezone(source) {
|
|
2503
|
+
return dayjs__default["default"](source).format("YYYY-MM-DD HH:mm:ss.SSSZ");
|
|
2501
2504
|
}
|
|
2502
2505
|
|
|
2503
2506
|
// TODO Generate mapper and cache it.
|
|
@@ -6260,7 +6263,7 @@ var SequenceAutoIncrementRecord = {
|
|
|
6260
6263
|
],
|
|
6261
6264
|
};
|
|
6262
6265
|
|
|
6263
|
-
var pluginModels$
|
|
6266
|
+
var pluginModels$7 = [SequenceRule, SequenceAutoIncrementRecord];
|
|
6264
6267
|
|
|
6265
6268
|
var generateSn = {
|
|
6266
6269
|
namespace: "svc",
|
|
@@ -6499,7 +6502,7 @@ class SequencePlugin$1 {
|
|
|
6499
6502
|
}
|
|
6500
6503
|
}
|
|
6501
6504
|
async configureModels(server, applicationConfig) {
|
|
6502
|
-
server.appendApplicationConfig({ models: pluginModels$
|
|
6505
|
+
server.appendApplicationConfig({ models: pluginModels$7 });
|
|
6503
6506
|
}
|
|
6504
6507
|
async configureServices(server, applicationConfig) {
|
|
6505
6508
|
this.#sequenceService = new SequenceService(server);
|
|
@@ -6983,7 +6986,7 @@ var AccessToken = {
|
|
|
6983
6986
|
],
|
|
6984
6987
|
};
|
|
6985
6988
|
|
|
6986
|
-
var pluginModels$
|
|
6989
|
+
var pluginModels$6 = [AccessToken];
|
|
6987
6990
|
|
|
6988
6991
|
var changePassword = {
|
|
6989
6992
|
namespace: "auth",
|
|
@@ -7103,7 +7106,7 @@ class AuthPlugin {
|
|
|
7103
7106
|
}
|
|
7104
7107
|
}
|
|
7105
7108
|
async configureModels(server, applicationConfig) {
|
|
7106
|
-
server.appendApplicationConfig({ models: pluginModels$
|
|
7109
|
+
server.appendApplicationConfig({ models: pluginModels$6 });
|
|
7107
7110
|
}
|
|
7108
7111
|
async configureServices(server, applicationConfig) {
|
|
7109
7112
|
this.#authService = new AuthService(server, server.config.jwtKey);
|
|
@@ -7378,7 +7381,7 @@ var getLicense$1 = /*#__PURE__*/Object.freeze({
|
|
|
7378
7381
|
|
|
7379
7382
|
var pluginActionHandlers$6 = [getLicense$1];
|
|
7380
7383
|
|
|
7381
|
-
var pluginModels$
|
|
7384
|
+
var pluginModels$5 = [];
|
|
7382
7385
|
|
|
7383
7386
|
var getLicense = {
|
|
7384
7387
|
namespace: "svc",
|
|
@@ -7549,7 +7552,7 @@ class LicensePlugin {
|
|
|
7549
7552
|
}
|
|
7550
7553
|
}
|
|
7551
7554
|
async configureModels(server, applicationConfig) {
|
|
7552
|
-
server.appendApplicationConfig({ models: pluginModels$
|
|
7555
|
+
server.appendApplicationConfig({ models: pluginModels$5 });
|
|
7553
7556
|
}
|
|
7554
7557
|
async configureServices(server, applicationConfig) {
|
|
7555
7558
|
this.#licenseService = new LicenseService(server, this.#encryptionKey);
|
|
@@ -7565,7 +7568,7 @@ class LicensePlugin {
|
|
|
7565
7568
|
|
|
7566
7569
|
var pluginActionHandlers$5 = [];
|
|
7567
7570
|
|
|
7568
|
-
var pluginModels$
|
|
7571
|
+
var pluginModels$4 = [];
|
|
7569
7572
|
|
|
7570
7573
|
var pluginRoutes$4 = [];
|
|
7571
7574
|
|
|
@@ -7631,7 +7634,7 @@ class MailPlugin {
|
|
|
7631
7634
|
}
|
|
7632
7635
|
}
|
|
7633
7636
|
async configureModels(server, applicationConfig) {
|
|
7634
|
-
server.appendApplicationConfig({ models: pluginModels$
|
|
7637
|
+
server.appendApplicationConfig({ models: pluginModels$4 });
|
|
7635
7638
|
}
|
|
7636
7639
|
async configureServices(server, applicationConfig) {
|
|
7637
7640
|
this.#mailService = new MailService(server, this.#config.smtpServer);
|
|
@@ -7704,7 +7707,7 @@ var Notification = {
|
|
|
7704
7707
|
],
|
|
7705
7708
|
};
|
|
7706
7709
|
|
|
7707
|
-
var pluginModels$
|
|
7710
|
+
var pluginModels$3 = [Notification];
|
|
7708
7711
|
|
|
7709
7712
|
var pluginRoutes$3 = [];
|
|
7710
7713
|
|
|
@@ -7756,7 +7759,7 @@ class SequencePlugin {
|
|
|
7756
7759
|
}
|
|
7757
7760
|
}
|
|
7758
7761
|
async configureModels(server, applicationConfig) {
|
|
7759
|
-
server.appendApplicationConfig({ models: pluginModels$
|
|
7762
|
+
server.appendApplicationConfig({ models: pluginModels$3 });
|
|
7760
7763
|
}
|
|
7761
7764
|
async configureServices(server, applicationConfig) {
|
|
7762
7765
|
this.#notificationService = new NotificationService(server);
|
|
@@ -8274,7 +8277,7 @@ var UserSettingItemSetting = {
|
|
|
8274
8277
|
],
|
|
8275
8278
|
};
|
|
8276
8279
|
|
|
8277
|
-
var pluginModels$
|
|
8280
|
+
var pluginModels$2 = [SystemSettingGroupSetting, SystemSettingItem, SystemSettingItemSetting, UserSettingGroupSetting, UserSettingItem, UserSettingItemSetting];
|
|
8278
8281
|
|
|
8279
8282
|
var getUserSettingValues = {
|
|
8280
8283
|
namespace: "svc",
|
|
@@ -8529,7 +8532,7 @@ class SettingPlugin {
|
|
|
8529
8532
|
}
|
|
8530
8533
|
}
|
|
8531
8534
|
async configureModels(server, applicationConfig) {
|
|
8532
|
-
server.appendApplicationConfig({ models: pluginModels$
|
|
8535
|
+
server.appendApplicationConfig({ models: pluginModels$2 });
|
|
8533
8536
|
}
|
|
8534
8537
|
async configureServices(server, applicationConfig) {
|
|
8535
8538
|
this.#settingService = new SettingService(server);
|
|
@@ -8541,31 +8544,154 @@ class SettingPlugin {
|
|
|
8541
8544
|
async onApplicationLoaded(server, applicationConfig) { }
|
|
8542
8545
|
}
|
|
8543
8546
|
|
|
8547
|
+
var CronJob = {
|
|
8548
|
+
maintainedBy: "cronJob",
|
|
8549
|
+
namespace: "sys",
|
|
8550
|
+
name: "sys_cron_job",
|
|
8551
|
+
singularCode: "sys_cron_job",
|
|
8552
|
+
pluralCode: "sys_cron_jobs",
|
|
8553
|
+
schema: "public",
|
|
8554
|
+
tableName: "sys_cron_jobs",
|
|
8555
|
+
properties: [
|
|
8556
|
+
{
|
|
8557
|
+
name: "id",
|
|
8558
|
+
code: "id",
|
|
8559
|
+
columnName: "id",
|
|
8560
|
+
type: "integer",
|
|
8561
|
+
required: true,
|
|
8562
|
+
autoIncrement: true,
|
|
8563
|
+
},
|
|
8564
|
+
{
|
|
8565
|
+
name: "code",
|
|
8566
|
+
code: "code",
|
|
8567
|
+
columnName: "code",
|
|
8568
|
+
type: "text",
|
|
8569
|
+
required: true,
|
|
8570
|
+
},
|
|
8571
|
+
{
|
|
8572
|
+
name: "description",
|
|
8573
|
+
code: "description",
|
|
8574
|
+
columnName: "description",
|
|
8575
|
+
type: "text",
|
|
8576
|
+
required: false,
|
|
8577
|
+
},
|
|
8578
|
+
{
|
|
8579
|
+
name: "cronTime",
|
|
8580
|
+
code: "cronTime",
|
|
8581
|
+
columnName: "cron_time",
|
|
8582
|
+
type: "text",
|
|
8583
|
+
required: true,
|
|
8584
|
+
},
|
|
8585
|
+
{
|
|
8586
|
+
name: "disabled",
|
|
8587
|
+
code: "disabled",
|
|
8588
|
+
columnName: "disabled",
|
|
8589
|
+
type: "boolean",
|
|
8590
|
+
required: true,
|
|
8591
|
+
defaultValue: "false",
|
|
8592
|
+
},
|
|
8593
|
+
{
|
|
8594
|
+
name: "jobOptions",
|
|
8595
|
+
code: "jobOptions",
|
|
8596
|
+
columnName: "job_options",
|
|
8597
|
+
type: "json",
|
|
8598
|
+
required: false,
|
|
8599
|
+
},
|
|
8600
|
+
{
|
|
8601
|
+
name: "isRunning",
|
|
8602
|
+
code: "isRunning",
|
|
8603
|
+
columnName: "is_running",
|
|
8604
|
+
type: "boolean",
|
|
8605
|
+
required: true,
|
|
8606
|
+
defaultValue: "false",
|
|
8607
|
+
},
|
|
8608
|
+
{
|
|
8609
|
+
name: "nextRunningTime",
|
|
8610
|
+
code: "nextRunningTime",
|
|
8611
|
+
columnName: "next_running_time",
|
|
8612
|
+
type: "datetime",
|
|
8613
|
+
required: false,
|
|
8614
|
+
},
|
|
8615
|
+
{
|
|
8616
|
+
name: "lastRunningTime",
|
|
8617
|
+
code: "lastRunningTime",
|
|
8618
|
+
columnName: "last_running_time",
|
|
8619
|
+
type: "datetime",
|
|
8620
|
+
required: false,
|
|
8621
|
+
},
|
|
8622
|
+
// success, failed, error
|
|
8623
|
+
{
|
|
8624
|
+
name: "lastRunningResult",
|
|
8625
|
+
code: "lastRunningResult",
|
|
8626
|
+
columnName: "last_running_result",
|
|
8627
|
+
type: "text",
|
|
8628
|
+
required: false,
|
|
8629
|
+
},
|
|
8630
|
+
{
|
|
8631
|
+
name: "lastErrorMessage",
|
|
8632
|
+
code: "lastErrorMessage",
|
|
8633
|
+
columnName: "last_error_message",
|
|
8634
|
+
type: "text",
|
|
8635
|
+
required: false,
|
|
8636
|
+
},
|
|
8637
|
+
{
|
|
8638
|
+
name: "lastErrorStack",
|
|
8639
|
+
code: "lastErrorStack",
|
|
8640
|
+
columnName: "last_error_stack",
|
|
8641
|
+
type: "text",
|
|
8642
|
+
required: false,
|
|
8643
|
+
},
|
|
8644
|
+
{
|
|
8645
|
+
name: "actionHandlerCode",
|
|
8646
|
+
code: "actionHandlerCode",
|
|
8647
|
+
columnName: "action_handler_code",
|
|
8648
|
+
type: "text",
|
|
8649
|
+
required: false,
|
|
8650
|
+
},
|
|
8651
|
+
{
|
|
8652
|
+
name: "handler",
|
|
8653
|
+
code: "handler",
|
|
8654
|
+
columnName: "handler",
|
|
8655
|
+
type: "text",
|
|
8656
|
+
required: false,
|
|
8657
|
+
},
|
|
8658
|
+
{
|
|
8659
|
+
name: "handleOptions",
|
|
8660
|
+
code: "handleOptions",
|
|
8661
|
+
columnName: "handle_options",
|
|
8662
|
+
type: "json",
|
|
8663
|
+
required: false,
|
|
8664
|
+
},
|
|
8665
|
+
{
|
|
8666
|
+
name: "onError",
|
|
8667
|
+
code: "onError",
|
|
8668
|
+
columnName: "on_error",
|
|
8669
|
+
type: "text",
|
|
8670
|
+
required: false,
|
|
8671
|
+
},
|
|
8672
|
+
],
|
|
8673
|
+
};
|
|
8674
|
+
|
|
8675
|
+
var pluginModels$1 = [CronJob];
|
|
8676
|
+
|
|
8544
8677
|
const code$1 = "runCronJob";
|
|
8545
8678
|
async function handler$1(plugin, ctx, options) {
|
|
8546
|
-
const { server, routerContext } = ctx;
|
|
8547
|
-
const { response } =
|
|
8679
|
+
const { server, routerContext: routeContext } = ctx;
|
|
8680
|
+
const { response } = routeContext;
|
|
8548
8681
|
const input = ctx.input;
|
|
8549
|
-
if (options
|
|
8682
|
+
if (options?.code) {
|
|
8550
8683
|
input.code = options.code;
|
|
8551
8684
|
}
|
|
8552
8685
|
if (!input.code) {
|
|
8553
8686
|
throw new Error(`Cron job code is required.`);
|
|
8554
8687
|
}
|
|
8555
8688
|
const cronJobService = server.getService("cronJobService");
|
|
8556
|
-
const
|
|
8557
|
-
if (!
|
|
8689
|
+
const jobConfig = cronJobService.getJobConfigurationByCode(input.code);
|
|
8690
|
+
if (!jobConfig) {
|
|
8558
8691
|
throw new Error(`Cron job with code '${input.code}' was not found.`);
|
|
8559
8692
|
}
|
|
8560
|
-
|
|
8561
|
-
|
|
8562
|
-
routerContext,
|
|
8563
|
-
next: null,
|
|
8564
|
-
server,
|
|
8565
|
-
applicationConfig: null,
|
|
8566
|
-
input: input.input,
|
|
8567
|
-
};
|
|
8568
|
-
await cronJobService.executeJob(jobExecutionContext, job);
|
|
8693
|
+
// running job in background.
|
|
8694
|
+
cronJobService.executeJob(jobConfig, input.input);
|
|
8569
8695
|
response.json({});
|
|
8570
8696
|
}
|
|
8571
8697
|
|
|
@@ -8593,9 +8719,30 @@ var runCronJob = {
|
|
|
8593
8719
|
|
|
8594
8720
|
var pluginRoutes$1 = [runCronJob];
|
|
8595
8721
|
|
|
8722
|
+
var cronJobEntityWatchers = [
|
|
8723
|
+
{
|
|
8724
|
+
eventName: "entity.update",
|
|
8725
|
+
modelSingularCode: "sys_cron_job",
|
|
8726
|
+
handler: async (ctx) => {
|
|
8727
|
+
const { server, payload, routerContext: routeContext } = ctx;
|
|
8728
|
+
const cronJobService = server.getService("cronJobService");
|
|
8729
|
+
const changes = payload.changes;
|
|
8730
|
+
const after = payload.after;
|
|
8731
|
+
await cronJobService.updateJobConfig(routeContext, {
|
|
8732
|
+
code: after.code,
|
|
8733
|
+
cronTime: changes.cronTime,
|
|
8734
|
+
disabled: changes.disabled,
|
|
8735
|
+
jobOptions: changes.jobOptions,
|
|
8736
|
+
});
|
|
8737
|
+
},
|
|
8738
|
+
},
|
|
8739
|
+
];
|
|
8740
|
+
|
|
8741
|
+
var pluginEntityWatchers = [...cronJobEntityWatchers];
|
|
8742
|
+
|
|
8596
8743
|
class CronJobService {
|
|
8597
8744
|
#server;
|
|
8598
|
-
#
|
|
8745
|
+
#namedJobInstances;
|
|
8599
8746
|
constructor(server) {
|
|
8600
8747
|
this.#server = server;
|
|
8601
8748
|
}
|
|
@@ -8607,79 +8754,207 @@ class CronJobService {
|
|
|
8607
8754
|
getJobConfigurationByCode(code) {
|
|
8608
8755
|
return lodash.find(this.#server.listCronJobs(), (job) => job.code === code);
|
|
8609
8756
|
}
|
|
8757
|
+
#createJobInstance(job) {
|
|
8758
|
+
return cron.CronJob.from({
|
|
8759
|
+
...(job.jobOptions || {}),
|
|
8760
|
+
cronTime: job.cronTime,
|
|
8761
|
+
onTick: async () => {
|
|
8762
|
+
await this.executeJob(job);
|
|
8763
|
+
},
|
|
8764
|
+
});
|
|
8765
|
+
}
|
|
8766
|
+
async #startJobInstance(routeContext, jobConfiguration, jobInstance) {
|
|
8767
|
+
const server = this.#server;
|
|
8768
|
+
const jobCode = jobConfiguration.code;
|
|
8769
|
+
const cronJobManager = server.getEntityManager("sys_cron_job");
|
|
8770
|
+
const cronJobInDb = await cronJobManager.findEntity({
|
|
8771
|
+
routeContext,
|
|
8772
|
+
filters: [{ operator: "eq", field: "code", value: jobCode }],
|
|
8773
|
+
});
|
|
8774
|
+
if (cronJobInDb) {
|
|
8775
|
+
let nextRunningTime;
|
|
8776
|
+
nextRunningTime = formatDateTimeWithTimezone(jobInstance.nextDate().toISO());
|
|
8777
|
+
await cronJobManager.updateEntityById({
|
|
8778
|
+
routeContext,
|
|
8779
|
+
id: cronJobInDb.id,
|
|
8780
|
+
entityToSave: {
|
|
8781
|
+
nextRunningTime,
|
|
8782
|
+
},
|
|
8783
|
+
});
|
|
8784
|
+
}
|
|
8785
|
+
jobInstance.start();
|
|
8786
|
+
}
|
|
8787
|
+
async #setJobNextRunningTime(routeContext, jobCode, nextRunningTime) {
|
|
8788
|
+
const server = this.#server;
|
|
8789
|
+
const cronJobManager = server.getEntityManager("sys_cron_job");
|
|
8790
|
+
const cronJobInDb = await cronJobManager.findEntity({
|
|
8791
|
+
routeContext,
|
|
8792
|
+
filters: [{ operator: "eq", field: "code", value: jobCode }],
|
|
8793
|
+
});
|
|
8794
|
+
await cronJobManager.updateEntityById({
|
|
8795
|
+
routeContext,
|
|
8796
|
+
id: cronJobInDb.id,
|
|
8797
|
+
entityToSave: {
|
|
8798
|
+
nextRunningTime,
|
|
8799
|
+
},
|
|
8800
|
+
});
|
|
8801
|
+
}
|
|
8610
8802
|
/**
|
|
8611
8803
|
* 重新加载定时任务
|
|
8612
8804
|
*/
|
|
8613
|
-
reloadJobs() {
|
|
8805
|
+
async reloadJobs() {
|
|
8614
8806
|
const server = this.#server;
|
|
8615
|
-
|
|
8616
|
-
|
|
8807
|
+
const routeContext = RouteContext.newSystemOperationContext(server);
|
|
8808
|
+
if (this.#namedJobInstances) {
|
|
8809
|
+
for (const job of this.#namedJobInstances) {
|
|
8617
8810
|
job.instance.stop();
|
|
8618
8811
|
}
|
|
8619
8812
|
}
|
|
8620
|
-
this.#
|
|
8621
|
-
const
|
|
8622
|
-
|
|
8623
|
-
|
|
8813
|
+
this.#namedJobInstances = [];
|
|
8814
|
+
const cronJobManager = server.getEntityManager("sys_cron_job");
|
|
8815
|
+
const cronJobConfigurationsInDb = await cronJobManager.findEntities({ routeContext });
|
|
8816
|
+
const cronJobConfigurations = server.listCronJobs();
|
|
8817
|
+
for (const cronJobConfig of cronJobConfigurations) {
|
|
8818
|
+
const jobCode = cronJobConfig.code;
|
|
8819
|
+
const jobConfigInDb = lodash.find(cronJobConfigurationsInDb, { code: jobCode });
|
|
8820
|
+
if (jobConfigInDb) {
|
|
8821
|
+
overrideJobConfig(cronJobConfig, jobConfigInDb);
|
|
8822
|
+
}
|
|
8823
|
+
if (cronJobConfig.disabled) {
|
|
8824
|
+
await this.#setJobNextRunningTime(routeContext, jobCode, null);
|
|
8624
8825
|
continue;
|
|
8625
8826
|
}
|
|
8626
|
-
const jobInstance =
|
|
8627
|
-
|
|
8628
|
-
|
|
8629
|
-
|
|
8630
|
-
await this.tryExecuteJob(job);
|
|
8631
|
-
},
|
|
8632
|
-
});
|
|
8633
|
-
jobInstance.start();
|
|
8634
|
-
this.#jobInstances.push({
|
|
8635
|
-
code: job.code,
|
|
8827
|
+
const jobInstance = this.#createJobInstance(cronJobConfig);
|
|
8828
|
+
await this.#startJobInstance(routeContext, cronJobConfig, jobInstance);
|
|
8829
|
+
this.#namedJobInstances.push({
|
|
8830
|
+
code: jobCode,
|
|
8636
8831
|
instance: jobInstance,
|
|
8637
8832
|
});
|
|
8638
8833
|
}
|
|
8639
8834
|
}
|
|
8640
|
-
|
|
8835
|
+
/**
|
|
8836
|
+
* 执行指定任务
|
|
8837
|
+
* @param job
|
|
8838
|
+
* @param input
|
|
8839
|
+
*/
|
|
8840
|
+
async executeJob(job, input) {
|
|
8641
8841
|
const server = this.#server;
|
|
8642
8842
|
const logger = server.getLogger();
|
|
8643
|
-
|
|
8843
|
+
const jobCode = job.code;
|
|
8844
|
+
logger.info(`Executing cron job '${jobCode}'...`);
|
|
8644
8845
|
let handlerContext = {
|
|
8645
8846
|
logger,
|
|
8646
8847
|
routerContext: RouteContext.newSystemOperationContext(server),
|
|
8647
8848
|
next: null,
|
|
8648
8849
|
server,
|
|
8649
8850
|
applicationConfig: null,
|
|
8650
|
-
input
|
|
8851
|
+
input,
|
|
8651
8852
|
};
|
|
8853
|
+
let result;
|
|
8854
|
+
let lastErrorMessage;
|
|
8855
|
+
let lastErrorStack;
|
|
8652
8856
|
try {
|
|
8653
|
-
|
|
8654
|
-
|
|
8857
|
+
validateLicense(server);
|
|
8858
|
+
if (job.actionHandlerCode) {
|
|
8859
|
+
const actionHandler = server.getActionHandlerByCode(job.code);
|
|
8860
|
+
await actionHandler(handlerContext, job.handleOptions);
|
|
8861
|
+
}
|
|
8862
|
+
else {
|
|
8863
|
+
await job.handler(handlerContext, job.handleOptions);
|
|
8864
|
+
}
|
|
8865
|
+
result = "success";
|
|
8866
|
+
logger.info(`Completed cron job '${jobCode}'...`);
|
|
8655
8867
|
}
|
|
8656
8868
|
catch (ex) {
|
|
8657
|
-
logger.error('Cron job "%s" execution error: %s',
|
|
8869
|
+
logger.error('Cron job "%s" execution error: %s', jobCode, ex.message);
|
|
8870
|
+
if (lodash.isString(ex)) {
|
|
8871
|
+
lastErrorMessage = ex;
|
|
8872
|
+
}
|
|
8873
|
+
else {
|
|
8874
|
+
lastErrorMessage = ex.message;
|
|
8875
|
+
lastErrorStack = ex.stack;
|
|
8876
|
+
}
|
|
8877
|
+
result = "failed";
|
|
8658
8878
|
if (job.onError) {
|
|
8659
8879
|
try {
|
|
8660
8880
|
await job.onError(handlerContext, ex);
|
|
8661
8881
|
}
|
|
8662
8882
|
catch (ex) {
|
|
8663
|
-
logger.error('Error handler of cron job "%s" execution failed: %s',
|
|
8883
|
+
logger.error('Error handler of cron job "%s" execution failed: %s', jobCode, ex.message);
|
|
8884
|
+
}
|
|
8885
|
+
}
|
|
8886
|
+
}
|
|
8887
|
+
try {
|
|
8888
|
+
const cronJobManager = server.getEntityManager("sys_cron_job");
|
|
8889
|
+
const cronJobInDb = await cronJobManager.findEntity({
|
|
8890
|
+
filters: [{ operator: "eq", field: "code", value: jobCode }],
|
|
8891
|
+
});
|
|
8892
|
+
if (cronJobInDb) {
|
|
8893
|
+
let nextRunningTime;
|
|
8894
|
+
const namedJobInstance = lodash.find(this.#namedJobInstances, { code: jobCode });
|
|
8895
|
+
if (namedJobInstance && namedJobInstance.instance) {
|
|
8896
|
+
nextRunningTime = formatDateTimeWithTimezone(namedJobInstance.instance.nextDate().toISO());
|
|
8664
8897
|
}
|
|
8898
|
+
await cronJobManager.updateEntityById({
|
|
8899
|
+
id: cronJobInDb.id,
|
|
8900
|
+
entityToSave: {
|
|
8901
|
+
nextRunningTime,
|
|
8902
|
+
lastRunningResult: result,
|
|
8903
|
+
lastRunningTime: getNowStringWithTimezone(),
|
|
8904
|
+
lastErrorMessage,
|
|
8905
|
+
lastErrorStack,
|
|
8906
|
+
},
|
|
8907
|
+
});
|
|
8665
8908
|
}
|
|
8666
8909
|
}
|
|
8910
|
+
catch (ex) {
|
|
8911
|
+
logger.error("Failed to saving cron job running result. job code: %s, error: %s", jobCode, ex.message);
|
|
8912
|
+
}
|
|
8667
8913
|
}
|
|
8668
|
-
|
|
8669
|
-
* 执行指定任务
|
|
8670
|
-
* @param job
|
|
8671
|
-
*/
|
|
8672
|
-
async executeJob(handlerContext, job) {
|
|
8914
|
+
async updateJobConfig(routeContext, options) {
|
|
8673
8915
|
const server = this.#server;
|
|
8674
|
-
|
|
8675
|
-
|
|
8676
|
-
|
|
8677
|
-
|
|
8916
|
+
const cronJobs = server.listCronJobs();
|
|
8917
|
+
const jobCode = options.code;
|
|
8918
|
+
if (!jobCode) {
|
|
8919
|
+
throw new Error(`options.code is required.`);
|
|
8920
|
+
}
|
|
8921
|
+
const cronJobConfig = lodash.find(cronJobs, { code: jobCode });
|
|
8922
|
+
if (!cronJobConfig) {
|
|
8923
|
+
throw new Error(`Cron job with code "${jobCode}" not found.`);
|
|
8924
|
+
}
|
|
8925
|
+
if (!["cronTime", "disabled", "jobOptions"].some((field) => !lodash.isNil(options[field]))) {
|
|
8926
|
+
return;
|
|
8927
|
+
}
|
|
8928
|
+
overrideJobConfig(cronJobConfig, options);
|
|
8929
|
+
const namedJobInstance = lodash.find(this.#namedJobInstances, { code: jobCode });
|
|
8930
|
+
if (namedJobInstance && namedJobInstance.instance) {
|
|
8931
|
+
namedJobInstance.instance.stop();
|
|
8932
|
+
namedJobInstance.instance = null;
|
|
8933
|
+
}
|
|
8934
|
+
if (cronJobConfig.disabled) {
|
|
8935
|
+
await this.#setJobNextRunningTime(routeContext, jobCode, null);
|
|
8678
8936
|
}
|
|
8679
8937
|
else {
|
|
8680
|
-
|
|
8938
|
+
const jobInstance = this.#createJobInstance(cronJobConfig);
|
|
8939
|
+
await this.#startJobInstance(routeContext, cronJobConfig, jobInstance);
|
|
8940
|
+
if (namedJobInstance) {
|
|
8941
|
+
namedJobInstance.instance = jobInstance;
|
|
8942
|
+
}
|
|
8943
|
+
else {
|
|
8944
|
+
this.#namedJobInstances.push({
|
|
8945
|
+
code: cronJobConfig.code,
|
|
8946
|
+
instance: jobInstance,
|
|
8947
|
+
});
|
|
8948
|
+
}
|
|
8681
8949
|
}
|
|
8682
8950
|
}
|
|
8951
|
+
}
|
|
8952
|
+
function overrideJobConfig(original, overrides) {
|
|
8953
|
+
["cronTime", "disabled", "jobOptions"].forEach((field) => {
|
|
8954
|
+
if (!lodash.isNil(overrides[field])) {
|
|
8955
|
+
original[field] = overrides[field];
|
|
8956
|
+
}
|
|
8957
|
+
});
|
|
8683
8958
|
}
|
|
8684
8959
|
|
|
8685
8960
|
class CronJobPlugin {
|
|
@@ -8710,11 +8985,16 @@ class CronJobPlugin {
|
|
|
8710
8985
|
server.registerActionHandler(this, actionHandler);
|
|
8711
8986
|
}
|
|
8712
8987
|
}
|
|
8713
|
-
async registerEventHandlers(server) {
|
|
8988
|
+
async registerEventHandlers(server) {
|
|
8989
|
+
for (const entityWatcher of pluginEntityWatchers)
|
|
8990
|
+
server.registerEntityWatcher(entityWatcher);
|
|
8991
|
+
}
|
|
8714
8992
|
async registerMessageHandlers(server) { }
|
|
8715
8993
|
async registerTaskProcessors(server) { }
|
|
8716
8994
|
async onLoadingApplication(server, applicationConfig) { }
|
|
8717
|
-
async configureModels(server, applicationConfig) {
|
|
8995
|
+
async configureModels(server, applicationConfig) {
|
|
8996
|
+
server.appendApplicationConfig({ models: pluginModels$1 });
|
|
8997
|
+
}
|
|
8718
8998
|
async configureModelProperties(server, applicationConfig) { }
|
|
8719
8999
|
async configureServices(server, applicationConfig) {
|
|
8720
9000
|
this.#cronJobService = new CronJobService(server);
|
|
@@ -8723,9 +9003,24 @@ class CronJobPlugin {
|
|
|
8723
9003
|
async configureRoutes(server, applicationConfig) {
|
|
8724
9004
|
server.appendApplicationConfig({ routes: pluginRoutes$1 });
|
|
8725
9005
|
}
|
|
8726
|
-
async onApplicationLoaded(server, applicationConfig) {
|
|
9006
|
+
async onApplicationLoaded(server, applicationConfig) {
|
|
9007
|
+
await saveCronJobsToDatabase(server);
|
|
9008
|
+
}
|
|
8727
9009
|
async onApplicationReady(server, applicationConfig) {
|
|
8728
|
-
this.#cronJobService.reloadJobs();
|
|
9010
|
+
await this.#cronJobService.reloadJobs();
|
|
9011
|
+
}
|
|
9012
|
+
}
|
|
9013
|
+
async function saveCronJobsToDatabase(server) {
|
|
9014
|
+
const cronJobManager = server.getEntityManager("sys_cron_job");
|
|
9015
|
+
for (const cronJobToSave of server.listCronJobs()) {
|
|
9016
|
+
const currentCronJob = await cronJobManager.findEntity({
|
|
9017
|
+
filters: [{ operator: "eq", field: "code", value: cronJobToSave.code }],
|
|
9018
|
+
});
|
|
9019
|
+
if (!currentCronJob) {
|
|
9020
|
+
await cronJobManager.createEntity({
|
|
9021
|
+
entity: lodash.pick(cronJobToSave, ["code", "description", "cronTime", "disabled"]),
|
|
9022
|
+
});
|
|
9023
|
+
}
|
|
8729
9024
|
}
|
|
8730
9025
|
}
|
|
8731
9026
|
|
|
@@ -9141,6 +9436,7 @@ exports.createJwt = createJwt;
|
|
|
9141
9436
|
exports.decodeJwt = decodeJwt;
|
|
9142
9437
|
exports.deleteCookie = deleteCookie;
|
|
9143
9438
|
exports.detectChangedFieldsOfEntity = detectChangedFieldsOfEntity;
|
|
9439
|
+
exports.formatDateTimeWithTimezone = formatDateTimeWithTimezone;
|
|
9144
9440
|
exports.generateJwtSecretKey = generateJwtSecretKey;
|
|
9145
9441
|
exports.getCookies = getCookies;
|
|
9146
9442
|
exports.getDateString = getDateString;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CronJob } from "cron";
|
|
2
|
+
import { CronJobConfiguration } from "../../types/cron-job-types";
|
|
2
3
|
export type RunCronJobActionHandlerOptions = {
|
|
3
4
|
code?: string;
|
|
4
5
|
};
|
|
@@ -10,3 +11,28 @@ export type NamedCronJobInstance = {
|
|
|
10
11
|
code: string;
|
|
11
12
|
instance: CronJob;
|
|
12
13
|
};
|
|
14
|
+
export type JobRunningResult = "success" | "failed" | "error";
|
|
15
|
+
export type SysCronJob = {
|
|
16
|
+
id: number;
|
|
17
|
+
code: string;
|
|
18
|
+
description: string;
|
|
19
|
+
cronTime: string;
|
|
20
|
+
disabled: boolean;
|
|
21
|
+
jobOptions: CronJobConfiguration["jobOptions"];
|
|
22
|
+
isRunning: boolean;
|
|
23
|
+
nextRunningTime: string;
|
|
24
|
+
lastRunningTime: string;
|
|
25
|
+
lastRunningResult?: JobRunningResult;
|
|
26
|
+
lastErrorMessage?: string;
|
|
27
|
+
lastErrorStack?: string;
|
|
28
|
+
actionHandlerCode?: string;
|
|
29
|
+
handler?: string;
|
|
30
|
+
handleOptions?: Record<string, any>;
|
|
31
|
+
onError?: string;
|
|
32
|
+
};
|
|
33
|
+
export type UpdateJobConfigOptions = {
|
|
34
|
+
code: string;
|
|
35
|
+
cronTime?: string;
|
|
36
|
+
disabled?: boolean;
|
|
37
|
+
jobOptions?: CronJobConfiguration["jobOptions"];
|
|
38
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { IRpdServer } from "../../../core/server";
|
|
2
|
+
import { RouteContext } from "../../../core/routeContext";
|
|
3
|
+
import { UpdateJobConfigOptions } from "../CronJobPluginTypes";
|
|
2
4
|
import { CronJobConfiguration } from "../../../types/cron-job-types";
|
|
3
|
-
import { ActionHandlerContext } from "../../../core/actionHandler";
|
|
4
5
|
export default class CronJobService {
|
|
5
6
|
#private;
|
|
6
7
|
constructor(server: IRpdServer);
|
|
@@ -13,11 +14,12 @@ export default class CronJobService {
|
|
|
13
14
|
/**
|
|
14
15
|
* 重新加载定时任务
|
|
15
16
|
*/
|
|
16
|
-
reloadJobs(): void
|
|
17
|
-
tryExecuteJob(job: CronJobConfiguration): Promise<void>;
|
|
17
|
+
reloadJobs(): Promise<void>;
|
|
18
18
|
/**
|
|
19
19
|
* 执行指定任务
|
|
20
20
|
* @param job
|
|
21
|
+
* @param input
|
|
21
22
|
*/
|
|
22
|
-
executeJob(
|
|
23
|
+
executeJob(job: CronJobConfiguration, input?: any): Promise<void>;
|
|
24
|
+
updateJobConfig(routeContext: RouteContext, options: UpdateJobConfigOptions): Promise<void>;
|
|
23
25
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { RpdApplicationConfig } from "~/types";
|
|
2
|
+
import pluginModels from "./models";
|
|
2
3
|
import pluginActionHandlers from "./actionHandlers";
|
|
3
4
|
import pluginRoutes from "./routes";
|
|
5
|
+
import pluginEntityWatchers from "./entityWatchers";
|
|
4
6
|
import {
|
|
5
7
|
IRpdServer,
|
|
6
8
|
RapidPlugin,
|
|
@@ -9,6 +11,8 @@ import {
|
|
|
9
11
|
RpdServerPluginExtendingAbilities,
|
|
10
12
|
} from "~/core/server";
|
|
11
13
|
import CronJobService from "./services/CronJobService";
|
|
14
|
+
import { SysCronJob } from "./CronJobPluginTypes";
|
|
15
|
+
import { pick } from "lodash";
|
|
12
16
|
|
|
13
17
|
class CronJobPlugin implements RapidPlugin {
|
|
14
18
|
#server: IRpdServer;
|
|
@@ -48,15 +52,18 @@ class CronJobPlugin implements RapidPlugin {
|
|
|
48
52
|
}
|
|
49
53
|
}
|
|
50
54
|
|
|
51
|
-
async registerEventHandlers(server: IRpdServer): Promise<any> {
|
|
52
|
-
|
|
55
|
+
async registerEventHandlers(server: IRpdServer): Promise<any> {
|
|
56
|
+
for (const entityWatcher of pluginEntityWatchers) server.registerEntityWatcher(entityWatcher);
|
|
57
|
+
}
|
|
53
58
|
async registerMessageHandlers(server: IRpdServer): Promise<any> {}
|
|
54
59
|
|
|
55
60
|
async registerTaskProcessors(server: IRpdServer): Promise<any> {}
|
|
56
61
|
|
|
57
62
|
async onLoadingApplication(server: IRpdServer, applicationConfig: RpdApplicationConfig): Promise<any> {}
|
|
58
63
|
|
|
59
|
-
async configureModels(server: IRpdServer, applicationConfig: RpdApplicationConfig): Promise<any> {
|
|
64
|
+
async configureModels(server: IRpdServer, applicationConfig: RpdApplicationConfig): Promise<any> {
|
|
65
|
+
server.appendApplicationConfig({ models: pluginModels });
|
|
66
|
+
}
|
|
60
67
|
|
|
61
68
|
async configureModelProperties(server: IRpdServer, applicationConfig: RpdApplicationConfig): Promise<any> {}
|
|
62
69
|
|
|
@@ -69,11 +76,29 @@ class CronJobPlugin implements RapidPlugin {
|
|
|
69
76
|
server.appendApplicationConfig({ routes: pluginRoutes });
|
|
70
77
|
}
|
|
71
78
|
|
|
72
|
-
async onApplicationLoaded(server: IRpdServer, applicationConfig: RpdApplicationConfig): Promise<any> {
|
|
79
|
+
async onApplicationLoaded(server: IRpdServer, applicationConfig: RpdApplicationConfig): Promise<any> {
|
|
80
|
+
await saveCronJobsToDatabase(server);
|
|
81
|
+
}
|
|
73
82
|
|
|
74
83
|
async onApplicationReady(server: IRpdServer, applicationConfig: RpdApplicationConfig): Promise<any> {
|
|
75
|
-
this.#cronJobService.reloadJobs();
|
|
84
|
+
await this.#cronJobService.reloadJobs();
|
|
76
85
|
}
|
|
77
86
|
}
|
|
78
87
|
|
|
79
88
|
export default CronJobPlugin;
|
|
89
|
+
|
|
90
|
+
async function saveCronJobsToDatabase(server: IRpdServer) {
|
|
91
|
+
const cronJobManager = server.getEntityManager<SysCronJob>("sys_cron_job");
|
|
92
|
+
|
|
93
|
+
for (const cronJobToSave of server.listCronJobs()) {
|
|
94
|
+
const currentCronJob = await cronJobManager.findEntity({
|
|
95
|
+
filters: [{ operator: "eq", field: "code", value: cronJobToSave.code }],
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
if (!currentCronJob) {
|
|
99
|
+
await cronJobManager.createEntity({
|
|
100
|
+
entity: pick(cronJobToSave, ["code", "description", "cronTime", "disabled"]),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CronJob } from "cron";
|
|
2
|
+
import { CronJobConfiguration } from "~/types/cron-job-types";
|
|
2
3
|
|
|
3
4
|
export type RunCronJobActionHandlerOptions = {
|
|
4
5
|
code?: string;
|
|
@@ -13,3 +14,31 @@ export type NamedCronJobInstance = {
|
|
|
13
14
|
code: string;
|
|
14
15
|
instance: CronJob;
|
|
15
16
|
};
|
|
17
|
+
|
|
18
|
+
export type JobRunningResult = "success" | "failed" | "error";
|
|
19
|
+
|
|
20
|
+
export type SysCronJob = {
|
|
21
|
+
id: number;
|
|
22
|
+
code: string;
|
|
23
|
+
description: string;
|
|
24
|
+
cronTime: string;
|
|
25
|
+
disabled: boolean;
|
|
26
|
+
jobOptions: CronJobConfiguration["jobOptions"];
|
|
27
|
+
isRunning: boolean;
|
|
28
|
+
nextRunningTime: string;
|
|
29
|
+
lastRunningTime: string;
|
|
30
|
+
lastRunningResult?: JobRunningResult;
|
|
31
|
+
lastErrorMessage?: string;
|
|
32
|
+
lastErrorStack?: string;
|
|
33
|
+
actionHandlerCode?: string;
|
|
34
|
+
handler?: string;
|
|
35
|
+
handleOptions?: Record<string, any>;
|
|
36
|
+
onError?: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type UpdateJobConfigOptions = {
|
|
40
|
+
code: string;
|
|
41
|
+
cronTime?: string;
|
|
42
|
+
disabled?: boolean;
|
|
43
|
+
jobOptions?: CronJobConfiguration["jobOptions"];
|
|
44
|
+
};
|
|
@@ -6,12 +6,12 @@ import CronJobService from "../services/CronJobService";
|
|
|
6
6
|
export const code = "runCronJob";
|
|
7
7
|
|
|
8
8
|
export async function handler(plugin: CronJobPlugin, ctx: ActionHandlerContext, options: RunCronJobActionHandlerOptions) {
|
|
9
|
-
const { server, routerContext } = ctx;
|
|
10
|
-
const { response } =
|
|
9
|
+
const { server, routerContext: routeContext } = ctx;
|
|
10
|
+
const { response } = routeContext;
|
|
11
11
|
|
|
12
12
|
const input: RunCronJobInput = ctx.input;
|
|
13
13
|
|
|
14
|
-
if (options
|
|
14
|
+
if (options?.code) {
|
|
15
15
|
input.code = options.code;
|
|
16
16
|
}
|
|
17
17
|
|
|
@@ -20,20 +20,13 @@ export async function handler(plugin: CronJobPlugin, ctx: ActionHandlerContext,
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
const cronJobService = server.getService<CronJobService>("cronJobService");
|
|
23
|
-
const
|
|
24
|
-
if (!
|
|
23
|
+
const jobConfig = cronJobService.getJobConfigurationByCode(input.code);
|
|
24
|
+
if (!jobConfig) {
|
|
25
25
|
throw new Error(`Cron job with code '${input.code}' was not found.`);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
routerContext,
|
|
31
|
-
next: null,
|
|
32
|
-
server,
|
|
33
|
-
applicationConfig: null,
|
|
34
|
-
input: input.input,
|
|
35
|
-
};
|
|
36
|
-
await cronJobService.executeJob(jobExecutionContext, job);
|
|
28
|
+
// running job in background.
|
|
29
|
+
cronJobService.executeJob(jobConfig, input.input);
|
|
37
30
|
|
|
38
31
|
response.json({});
|
|
39
32
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { EntityWatcher, EntityWatchHandlerContext } from "~/types";
|
|
2
|
+
import CronJobService from "../services/CronJobService";
|
|
3
|
+
import { SysCronJob } from "../CronJobPluginTypes";
|
|
4
|
+
|
|
5
|
+
export default [
|
|
6
|
+
{
|
|
7
|
+
eventName: "entity.update",
|
|
8
|
+
modelSingularCode: "sys_cron_job",
|
|
9
|
+
handler: async (ctx: EntityWatchHandlerContext<"entity.update">) => {
|
|
10
|
+
const { server, payload, routerContext: routeContext } = ctx;
|
|
11
|
+
|
|
12
|
+
const cronJobService = server.getService<CronJobService>("cronJobService");
|
|
13
|
+
|
|
14
|
+
const changes: Partial<SysCronJob> = payload.changes;
|
|
15
|
+
const after: SysCronJob = payload.after;
|
|
16
|
+
await cronJobService.updateJobConfig(routeContext, {
|
|
17
|
+
code: after.code,
|
|
18
|
+
cronTime: changes.cronTime,
|
|
19
|
+
disabled: changes.disabled,
|
|
20
|
+
jobOptions: changes.jobOptions,
|
|
21
|
+
});
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
] satisfies EntityWatcher<any>[];
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { RpdDataModel } from "~/types";
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
maintainedBy: "cronJob",
|
|
5
|
+
namespace: "sys",
|
|
6
|
+
name: "sys_cron_job",
|
|
7
|
+
singularCode: "sys_cron_job",
|
|
8
|
+
pluralCode: "sys_cron_jobs",
|
|
9
|
+
schema: "public",
|
|
10
|
+
tableName: "sys_cron_jobs",
|
|
11
|
+
properties: [
|
|
12
|
+
{
|
|
13
|
+
name: "id",
|
|
14
|
+
code: "id",
|
|
15
|
+
columnName: "id",
|
|
16
|
+
type: "integer",
|
|
17
|
+
required: true,
|
|
18
|
+
autoIncrement: true,
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: "code",
|
|
22
|
+
code: "code",
|
|
23
|
+
columnName: "code",
|
|
24
|
+
type: "text",
|
|
25
|
+
required: true,
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: "description",
|
|
29
|
+
code: "description",
|
|
30
|
+
columnName: "description",
|
|
31
|
+
type: "text",
|
|
32
|
+
required: false,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: "cronTime",
|
|
36
|
+
code: "cronTime",
|
|
37
|
+
columnName: "cron_time",
|
|
38
|
+
type: "text",
|
|
39
|
+
required: true,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: "disabled",
|
|
43
|
+
code: "disabled",
|
|
44
|
+
columnName: "disabled",
|
|
45
|
+
type: "boolean",
|
|
46
|
+
required: true,
|
|
47
|
+
defaultValue: "false",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: "jobOptions",
|
|
51
|
+
code: "jobOptions",
|
|
52
|
+
columnName: "job_options",
|
|
53
|
+
type: "json",
|
|
54
|
+
required: false,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: "isRunning",
|
|
58
|
+
code: "isRunning",
|
|
59
|
+
columnName: "is_running",
|
|
60
|
+
type: "boolean",
|
|
61
|
+
required: true,
|
|
62
|
+
defaultValue: "false",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: "nextRunningTime",
|
|
66
|
+
code: "nextRunningTime",
|
|
67
|
+
columnName: "next_running_time",
|
|
68
|
+
type: "datetime",
|
|
69
|
+
required: false,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: "lastRunningTime",
|
|
73
|
+
code: "lastRunningTime",
|
|
74
|
+
columnName: "last_running_time",
|
|
75
|
+
type: "datetime",
|
|
76
|
+
required: false,
|
|
77
|
+
},
|
|
78
|
+
// success, failed, error
|
|
79
|
+
{
|
|
80
|
+
name: "lastRunningResult",
|
|
81
|
+
code: "lastRunningResult",
|
|
82
|
+
columnName: "last_running_result",
|
|
83
|
+
type: "text",
|
|
84
|
+
required: false,
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: "lastErrorMessage",
|
|
88
|
+
code: "lastErrorMessage",
|
|
89
|
+
columnName: "last_error_message",
|
|
90
|
+
type: "text",
|
|
91
|
+
required: false,
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: "lastErrorStack",
|
|
95
|
+
code: "lastErrorStack",
|
|
96
|
+
columnName: "last_error_stack",
|
|
97
|
+
type: "text",
|
|
98
|
+
required: false,
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: "actionHandlerCode",
|
|
102
|
+
code: "actionHandlerCode",
|
|
103
|
+
columnName: "action_handler_code",
|
|
104
|
+
type: "text",
|
|
105
|
+
required: false,
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: "handler",
|
|
109
|
+
code: "handler",
|
|
110
|
+
columnName: "handler",
|
|
111
|
+
type: "text",
|
|
112
|
+
required: false,
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
name: "handleOptions",
|
|
116
|
+
code: "handleOptions",
|
|
117
|
+
columnName: "handle_options",
|
|
118
|
+
type: "json",
|
|
119
|
+
required: false,
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: "onError",
|
|
123
|
+
code: "onError",
|
|
124
|
+
columnName: "on_error",
|
|
125
|
+
type: "text",
|
|
126
|
+
required: false,
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
} as RpdDataModel;
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { CronJob } from "cron";
|
|
2
2
|
import { IRpdServer } from "~/core/server";
|
|
3
|
-
import { find } from "lodash";
|
|
3
|
+
import { find, isNil, isString, Many } from "lodash";
|
|
4
4
|
import { RouteContext } from "~/core/routeContext";
|
|
5
5
|
import { validateLicense } from "~/helpers/licenseHelper";
|
|
6
|
-
import { NamedCronJobInstance } from "../CronJobPluginTypes";
|
|
6
|
+
import { JobRunningResult, NamedCronJobInstance, SysCronJob, UpdateJobConfigOptions } from "../CronJobPluginTypes";
|
|
7
7
|
import { CronJobConfiguration } from "~/types/cron-job-types";
|
|
8
8
|
import { ActionHandlerContext } from "~/core/actionHandler";
|
|
9
|
+
import { formatDateTimeWithTimezone, getNowStringWithTimezone } from "~/utilities/timeUtility";
|
|
9
10
|
|
|
10
11
|
export default class CronJobService {
|
|
11
12
|
#server: IRpdServer;
|
|
12
|
-
#
|
|
13
|
+
#namedJobInstances: NamedCronJobInstance[];
|
|
13
14
|
|
|
14
15
|
constructor(server: IRpdServer) {
|
|
15
16
|
this.#server = server;
|
|
@@ -24,46 +25,109 @@ export default class CronJobService {
|
|
|
24
25
|
return find(this.#server.listCronJobs(), (job) => job.code === code);
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
#createJobInstance(job: CronJobConfiguration) {
|
|
29
|
+
return CronJob.from({
|
|
30
|
+
...(job.jobOptions || {}),
|
|
31
|
+
cronTime: job.cronTime,
|
|
32
|
+
onTick: async () => {
|
|
33
|
+
await this.executeJob(job);
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async #startJobInstance(routeContext: RouteContext, jobConfiguration: CronJobConfiguration, jobInstance: CronJob) {
|
|
39
|
+
const server = this.#server;
|
|
40
|
+
const jobCode = jobConfiguration.code;
|
|
41
|
+
const cronJobManager = server.getEntityManager<SysCronJob>("sys_cron_job");
|
|
42
|
+
const cronJobInDb = await cronJobManager.findEntity({
|
|
43
|
+
routeContext,
|
|
44
|
+
filters: [{ operator: "eq", field: "code", value: jobCode }],
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
if (cronJobInDb) {
|
|
48
|
+
let nextRunningTime: string | null;
|
|
49
|
+
nextRunningTime = formatDateTimeWithTimezone(jobInstance.nextDate().toISO());
|
|
50
|
+
|
|
51
|
+
await cronJobManager.updateEntityById({
|
|
52
|
+
routeContext,
|
|
53
|
+
id: cronJobInDb.id,
|
|
54
|
+
entityToSave: {
|
|
55
|
+
nextRunningTime,
|
|
56
|
+
} as Partial<SysCronJob>,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
jobInstance.start();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async #setJobNextRunningTime(routeContext: RouteContext, jobCode: string, nextRunningTime: string | null) {
|
|
64
|
+
const server = this.#server;
|
|
65
|
+
const cronJobManager = server.getEntityManager<SysCronJob>("sys_cron_job");
|
|
66
|
+
const cronJobInDb = await cronJobManager.findEntity({
|
|
67
|
+
routeContext,
|
|
68
|
+
filters: [{ operator: "eq", field: "code", value: jobCode }],
|
|
69
|
+
});
|
|
70
|
+
await cronJobManager.updateEntityById({
|
|
71
|
+
routeContext,
|
|
72
|
+
id: cronJobInDb.id,
|
|
73
|
+
entityToSave: {
|
|
74
|
+
nextRunningTime,
|
|
75
|
+
} as Partial<SysCronJob>,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
27
79
|
/**
|
|
28
80
|
* 重新加载定时任务
|
|
29
81
|
*/
|
|
30
|
-
reloadJobs() {
|
|
82
|
+
async reloadJobs() {
|
|
31
83
|
const server = this.#server;
|
|
84
|
+
const routeContext = RouteContext.newSystemOperationContext(server);
|
|
32
85
|
|
|
33
|
-
if (this.#
|
|
34
|
-
for (const job of this.#
|
|
86
|
+
if (this.#namedJobInstances) {
|
|
87
|
+
for (const job of this.#namedJobInstances) {
|
|
35
88
|
job.instance.stop();
|
|
36
89
|
}
|
|
37
90
|
}
|
|
38
91
|
|
|
39
|
-
this.#
|
|
92
|
+
this.#namedJobInstances = [];
|
|
40
93
|
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
94
|
+
const cronJobManager = server.getEntityManager<SysCronJob>("sys_cron_job");
|
|
95
|
+
const cronJobConfigurationsInDb = await cronJobManager.findEntities({ routeContext });
|
|
96
|
+
|
|
97
|
+
const cronJobConfigurations = server.listCronJobs();
|
|
98
|
+
for (const cronJobConfig of cronJobConfigurations) {
|
|
99
|
+
const jobCode = cronJobConfig.code;
|
|
100
|
+
const jobConfigInDb = find(cronJobConfigurationsInDb, { code: jobCode });
|
|
101
|
+
if (jobConfigInDb) {
|
|
102
|
+
overrideJobConfig(cronJobConfig, jobConfigInDb);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (cronJobConfig.disabled) {
|
|
106
|
+
await this.#setJobNextRunningTime(routeContext, jobCode, null);
|
|
44
107
|
continue;
|
|
45
108
|
}
|
|
46
109
|
|
|
47
|
-
const jobInstance =
|
|
48
|
-
|
|
49
|
-
cronTime: job.cronTime,
|
|
50
|
-
onTick: async () => {
|
|
51
|
-
await this.tryExecuteJob(job);
|
|
52
|
-
},
|
|
53
|
-
});
|
|
54
|
-
jobInstance.start();
|
|
110
|
+
const jobInstance = this.#createJobInstance(cronJobConfig);
|
|
111
|
+
await this.#startJobInstance(routeContext, cronJobConfig, jobInstance);
|
|
55
112
|
|
|
56
|
-
this.#
|
|
57
|
-
code:
|
|
113
|
+
this.#namedJobInstances.push({
|
|
114
|
+
code: jobCode,
|
|
58
115
|
instance: jobInstance,
|
|
59
116
|
});
|
|
60
117
|
}
|
|
61
118
|
}
|
|
62
119
|
|
|
63
|
-
|
|
120
|
+
/**
|
|
121
|
+
* 执行指定任务
|
|
122
|
+
* @param job
|
|
123
|
+
* @param input
|
|
124
|
+
*/
|
|
125
|
+
async executeJob(job: CronJobConfiguration, input?: any) {
|
|
64
126
|
const server = this.#server;
|
|
65
127
|
const logger = server.getLogger();
|
|
66
|
-
|
|
128
|
+
|
|
129
|
+
const jobCode = job.code;
|
|
130
|
+
logger.info(`Executing cron job '${jobCode}'...`);
|
|
67
131
|
|
|
68
132
|
let handlerContext: ActionHandlerContext = {
|
|
69
133
|
logger,
|
|
@@ -71,38 +135,118 @@ export default class CronJobService {
|
|
|
71
135
|
next: null,
|
|
72
136
|
server,
|
|
73
137
|
applicationConfig: null,
|
|
74
|
-
input
|
|
138
|
+
input,
|
|
75
139
|
};
|
|
76
140
|
|
|
141
|
+
let result: JobRunningResult;
|
|
142
|
+
let lastErrorMessage: string | null;
|
|
143
|
+
let lastErrorStack: string | null;
|
|
77
144
|
try {
|
|
78
|
-
|
|
79
|
-
|
|
145
|
+
validateLicense(server);
|
|
146
|
+
|
|
147
|
+
if (job.actionHandlerCode) {
|
|
148
|
+
const actionHandler = server.getActionHandlerByCode(job.code);
|
|
149
|
+
await actionHandler(handlerContext, job.handleOptions);
|
|
150
|
+
} else {
|
|
151
|
+
await job.handler(handlerContext, job.handleOptions);
|
|
152
|
+
}
|
|
153
|
+
result = "success";
|
|
154
|
+
logger.info(`Completed cron job '${jobCode}'...`);
|
|
80
155
|
} catch (ex: any) {
|
|
81
|
-
logger.error('Cron job "%s" execution error: %s',
|
|
156
|
+
logger.error('Cron job "%s" execution error: %s', jobCode, ex.message);
|
|
157
|
+
if (isString(ex)) {
|
|
158
|
+
lastErrorMessage = ex;
|
|
159
|
+
} else {
|
|
160
|
+
lastErrorMessage = ex.message;
|
|
161
|
+
lastErrorStack = ex.stack;
|
|
162
|
+
}
|
|
163
|
+
result = "failed";
|
|
82
164
|
|
|
83
165
|
if (job.onError) {
|
|
84
166
|
try {
|
|
85
167
|
await job.onError(handlerContext, ex);
|
|
86
168
|
} catch (ex) {
|
|
87
|
-
logger.error('Error handler of cron job "%s" execution failed: %s',
|
|
169
|
+
logger.error('Error handler of cron job "%s" execution failed: %s', jobCode, ex.message);
|
|
88
170
|
}
|
|
89
171
|
}
|
|
90
172
|
}
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const cronJobManager = server.getEntityManager<SysCronJob>("sys_cron_job");
|
|
176
|
+
const cronJobInDb = await cronJobManager.findEntity({
|
|
177
|
+
filters: [{ operator: "eq", field: "code", value: jobCode }],
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
if (cronJobInDb) {
|
|
181
|
+
let nextRunningTime: string | null;
|
|
182
|
+
const namedJobInstance = find(this.#namedJobInstances, { code: jobCode });
|
|
183
|
+
if (namedJobInstance && namedJobInstance.instance) {
|
|
184
|
+
nextRunningTime = formatDateTimeWithTimezone(namedJobInstance.instance.nextDate().toISO());
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
await cronJobManager.updateEntityById({
|
|
188
|
+
id: cronJobInDb.id,
|
|
189
|
+
entityToSave: {
|
|
190
|
+
nextRunningTime,
|
|
191
|
+
lastRunningResult: result,
|
|
192
|
+
lastRunningTime: getNowStringWithTimezone(),
|
|
193
|
+
lastErrorMessage,
|
|
194
|
+
lastErrorStack,
|
|
195
|
+
} as Partial<SysCronJob>,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
} catch (ex: any) {
|
|
199
|
+
logger.error("Failed to saving cron job running result. job code: %s, error: %s", jobCode, ex.message);
|
|
200
|
+
}
|
|
91
201
|
}
|
|
92
202
|
|
|
93
|
-
|
|
94
|
-
* 执行指定任务
|
|
95
|
-
* @param job
|
|
96
|
-
*/
|
|
97
|
-
async executeJob(handlerContext: ActionHandlerContext, job: CronJobConfiguration) {
|
|
203
|
+
async updateJobConfig(routeContext: RouteContext, options: UpdateJobConfigOptions) {
|
|
98
204
|
const server = this.#server;
|
|
99
|
-
|
|
205
|
+
const cronJobs = server.listCronJobs();
|
|
206
|
+
const jobCode = options.code;
|
|
207
|
+
if (!jobCode) {
|
|
208
|
+
throw new Error(`options.code is required.`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const cronJobConfig = find(cronJobs, { code: jobCode });
|
|
212
|
+
if (!cronJobConfig) {
|
|
213
|
+
throw new Error(`Cron job with code "${jobCode}" not found.`);
|
|
214
|
+
}
|
|
100
215
|
|
|
101
|
-
if (
|
|
102
|
-
|
|
103
|
-
|
|
216
|
+
if (!(["cronTime", "disabled", "jobOptions"] as (keyof typeof options)[]).some((field) => !isNil(options[field]))) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
overrideJobConfig(cronJobConfig, options);
|
|
221
|
+
|
|
222
|
+
const namedJobInstance = find(this.#namedJobInstances, { code: jobCode });
|
|
223
|
+
if (namedJobInstance && namedJobInstance.instance) {
|
|
224
|
+
namedJobInstance.instance.stop();
|
|
225
|
+
namedJobInstance.instance = null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (cronJobConfig.disabled) {
|
|
229
|
+
await this.#setJobNextRunningTime(routeContext, jobCode, null);
|
|
104
230
|
} else {
|
|
105
|
-
|
|
231
|
+
const jobInstance = this.#createJobInstance(cronJobConfig);
|
|
232
|
+
await this.#startJobInstance(routeContext, cronJobConfig, jobInstance);
|
|
233
|
+
|
|
234
|
+
if (namedJobInstance) {
|
|
235
|
+
namedJobInstance.instance = jobInstance;
|
|
236
|
+
} else {
|
|
237
|
+
this.#namedJobInstances.push({
|
|
238
|
+
code: cronJobConfig.code,
|
|
239
|
+
instance: jobInstance,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
106
242
|
}
|
|
107
243
|
}
|
|
108
244
|
}
|
|
245
|
+
|
|
246
|
+
function overrideJobConfig(original: Partial<SysCronJob> | CronJobConfiguration, overrides: Partial<SysCronJob>) {
|
|
247
|
+
(["cronTime", "disabled", "jobOptions"] as (keyof typeof overrides)[]).forEach((field: string) => {
|
|
248
|
+
if (!isNil(overrides[field])) {
|
|
249
|
+
original[field] = overrides[field];
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
}
|
|
@@ -11,3 +11,7 @@ export function getNowStringWithTimezone() {
|
|
|
11
11
|
export function getDateString(timeString) {
|
|
12
12
|
return dayjs(timeString).format("YYYY-MM-DD");
|
|
13
13
|
}
|
|
14
|
+
|
|
15
|
+
export function formatDateTimeWithTimezone(source: any) {
|
|
16
|
+
return dayjs(source).format("YYYY-MM-DD HH:mm:ss.SSSZ");
|
|
17
|
+
}
|