@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 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$6 = [SequenceRule, SequenceAutoIncrementRecord];
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$6 });
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$5 = [AccessToken];
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$5 });
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$4 = [];
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$4 });
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$3 = [];
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$3 });
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$2 = [Notification];
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$2 });
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$1 = [SystemSettingGroupSetting, SystemSettingItem, SystemSettingItemSetting, UserSettingGroupSetting, UserSettingItem, UserSettingItemSetting];
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$1 });
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 } = routerContext;
8679
+ const { server, routerContext: routeContext } = ctx;
8680
+ const { response } = routeContext;
8548
8681
  const input = ctx.input;
8549
- if (options.code) {
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 job = cronJobService.getJobConfigurationByCode(input.code);
8557
- if (!job) {
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
- let jobExecutionContext = {
8561
- logger: server.getLogger(),
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
- #jobInstances;
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
- if (this.#jobInstances) {
8616
- for (const job of this.#jobInstances) {
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.#jobInstances = [];
8621
- const cronJobs = server.listCronJobs();
8622
- for (const job of cronJobs) {
8623
- if (job.disabled) {
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 = cron.CronJob.from({
8627
- ...(job.jobOptions || {}),
8628
- cronTime: job.cronTime,
8629
- onTick: async () => {
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
- async tryExecuteJob(job) {
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
- logger.info(`Executing cron job '${job.code}'...`);
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: null,
8851
+ input,
8651
8852
  };
8853
+ let result;
8854
+ let lastErrorMessage;
8855
+ let lastErrorStack;
8652
8856
  try {
8653
- await this.executeJob(handlerContext, job);
8654
- logger.info(`Completed cron job '${job.code}'...`);
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', job.code, ex.message);
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', job.code, ex.message);
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
- validateLicense(server);
8675
- if (job.actionHandlerCode) {
8676
- const actionHandler = server.getActionHandlerByCode(job.code);
8677
- await actionHandler(handlerContext, job.handleOptions);
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
- await job.handler(handlerContext, job.handleOptions);
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
+ };
@@ -0,0 +1,7 @@
1
+ import type { EntityWatchHandlerContext } from "../../../types";
2
+ declare const _default: {
3
+ eventName: string;
4
+ modelSingularCode: string;
5
+ handler: (ctx: EntityWatchHandlerContext<"entity.update">) => Promise<void>;
6
+ }[];
7
+ export default _default;
@@ -0,0 +1,6 @@
1
+ declare const _default: {
2
+ eventName: string;
3
+ modelSingularCode: string;
4
+ handler: (ctx: import("../../../types").EntityWatchHandlerContext<"entity.update">) => Promise<void>;
5
+ }[];
6
+ export default _default;
@@ -0,0 +1,3 @@
1
+ import { RpdDataModel } from "../../../types";
2
+ declare const _default: RpdDataModel;
3
+ export default _default;
@@ -0,0 +1,2 @@
1
+ declare const _default: import("../../..").RpdDataModel[];
2
+ export default _default;
@@ -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(handlerContext: ActionHandlerContext, job: CronJobConfiguration): Promise<void>;
23
+ executeJob(job: CronJobConfiguration, input?: any): Promise<void>;
24
+ updateJobConfig(routeContext: RouteContext, options: UpdateJobConfigOptions): Promise<void>;
23
25
  }
@@ -1,3 +1,4 @@
1
1
  export declare function getNowString(): string;
2
2
  export declare function getNowStringWithTimezone(): string;
3
3
  export declare function getDateString(timeString: any): string;
4
+ export declare function formatDateTimeWithTimezone(source: any): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ruiapp/rapid-core",
3
- "version": "0.8.10",
3
+ "version": "0.8.12",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -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 } = routerContext;
9
+ const { server, routerContext: routeContext } = ctx;
10
+ const { response } = routeContext;
11
11
 
12
12
  const input: RunCronJobInput = ctx.input;
13
13
 
14
- if (options.code) {
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 job = cronJobService.getJobConfigurationByCode(input.code);
24
- if (!job) {
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
- let jobExecutionContext: ActionHandlerContext = {
29
- logger: server.getLogger(),
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,4 @@
1
+ import { EntityWatcher } from "~/types";
2
+ import cronJobEntityWatchers from "./cronJobEntityWatchers";
3
+
4
+ export default [...cronJobEntityWatchers] 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;
@@ -0,0 +1,3 @@
1
+ import CronJob from "./CronJob";
2
+
3
+ export default [CronJob];
@@ -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
- #jobInstances: NamedCronJobInstance[];
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.#jobInstances) {
34
- for (const job of this.#jobInstances) {
86
+ if (this.#namedJobInstances) {
87
+ for (const job of this.#namedJobInstances) {
35
88
  job.instance.stop();
36
89
  }
37
90
  }
38
91
 
39
- this.#jobInstances = [];
92
+ this.#namedJobInstances = [];
40
93
 
41
- const cronJobs = server.listCronJobs();
42
- for (const job of cronJobs) {
43
- if (job.disabled) {
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 = CronJob.from({
48
- ...(job.jobOptions || {}),
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.#jobInstances.push({
57
- code: job.code,
113
+ this.#namedJobInstances.push({
114
+ code: jobCode,
58
115
  instance: jobInstance,
59
116
  });
60
117
  }
61
118
  }
62
119
 
63
- async tryExecuteJob(job: CronJobConfiguration) {
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
- logger.info(`Executing cron job '${job.code}'...`);
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: null,
138
+ input,
75
139
  };
76
140
 
141
+ let result: JobRunningResult;
142
+ let lastErrorMessage: string | null;
143
+ let lastErrorStack: string | null;
77
144
  try {
78
- await this.executeJob(handlerContext, job);
79
- logger.info(`Completed cron job '${job.code}'...`);
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', job.code, ex.message);
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', job.code, ex.message);
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
- validateLicense(server);
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 (job.actionHandlerCode) {
102
- const actionHandler = server.getActionHandlerByCode(job.code);
103
- await actionHandler(handlerContext, job.handleOptions);
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
- await job.handler(handlerContext, job.handleOptions);
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
+ }